> built 2026-10-14 13:03 UTC from a43715b (main) · lacing 0.0.54. Details: build_info.json

# index.html.md

<!-- generated by epythet -->

# lacing

A standoff, interval-keyed annotation system. Pythonic core: a
`MutableMapping[TimeInterval, list[Annotation]]` facade with rational time,
ELAN-style tier stereotypes, and Allen’s interval algebra. Designed for
time-based media (audio, video, speech, music) but generalizes to any 1-D
interval domain.

> **Status:** Phase 0–2 complete. Core data model, in-memory + SQLite +
> Postgres stores, **eight round-trip adapters** (Praat TextGrid, WebVTT,
> W3C Web Annotation, `.annot` SQLite, ELAN EAF, JAMS, Label Studio JSON,
> OpenTimelineIO), body-schema registry + JSON Schema export + migrations,
> inter-annotator agreement metrics, a `lacing` CLI, a **FastAPI HTTP
> server** (REST CRUD + ETag + import/export + schemas + op-log +
> `/state-at` time-travel), an **MCP server** (10 tools, agents as
> first-class clients), a **processor registry** (`low_confidence_review`,
> `detect_density_change_points`) with optional **Arq** integration, and
> opt-in **OpenTelemetry** instrumentation. Frontend is on the roadmap
> (see `misc/docs/Lacing Development Roadmap.md`).

## Install

```bash
pip install lacing                # core only
pip install 'lacing[textgrid]'    # + Praat TextGrid support (praatio)
pip install 'lacing[eaf]'         # + ELAN EAF support (pympi-ling)
pip install 'lacing[jams]'        # + JAMS (MIR annotation) support
pip install 'lacing[postgres]'    # + PostgresStore (psycopg + GiST + EXCLUDE)
pip install 'lacing[server]'      # + FastAPI HTTP server
pip install 'lacing[mcp]'         # + MCP server (agents as first-class clients)
pip install 'lacing[arq]'         # + Arq background workers (Redis-backed)
pip install 'lacing[otio]'        # + OpenTimelineIO adapter
pip install 'lacing[otel]'        # + OpenTelemetry instrumentation
```

## 30-second tour

```python
from lacing.adapters import textgrid, webvtt, web_annotation  # registers each
from lacing.adapters import load, dump

# Load a Praat TextGrid → an in-memory store keyed by interval
store = load("speech.TextGrid", rate=1000)

# Query overlaps using Allen's relations
from lacing.time import RationalTime, TimeInterval

window = TimeInterval(RationalTime(500, 1000), RationalTime(1500, 1000))

for ann in store.intersects(window):
    print(ann.tier, ann.body["text"])

for ann in store.during(window):  # strictly inside the window
    ...

# Save out as WebVTT
dump(store, "speech.vtt", format="webvtt")

# Or as W3C Web Annotation JSON-LD
dump(store, "speech.jsonld", format="web_annotation")
```

## Track facades — opinionated bundles of tiers

Some tier shapes recur often enough to deserve a friendly builder.
`lacing.tracks.subtitle` is the first: a `(sections, lines, words)`
trio over one audio asset, with float-second times and the
`Annotation` / `MediaRef` / `Provenance` plumbing hidden:

```python
from lacing import MemoryStore
from lacing.tracks.subtitle import SubtitleBuilder, SubtitleTrack

store = MemoryStore()
with SubtitleBuilder(store, asset_id="song/audio.mp3") as b:
    b.section("intro", 0.0, 12.5)
    b.section("verse_1", 12.5, 35.0)
    b.line(
        "I came down to the river",
        12.5,
        16.2,
        section="verse_1",
        line_index=0,
        words=[
            ("I", 12.5, 12.7),
            ("came", 12.7, 13.0, 0.95),  # optional confidence
            ("down", 13.0, 13.3),
        ],
    )

track = SubtitleTrack(store, asset_id="song/audio.mp3")
track.lines_in(15.0, 17.0)  # lines overlapping the window
track.words_in(12.5, 13.5)  # words overlapping the window
track.sections_covering(20.0)  # sections containing this instant
```

The facade reuses `at_tier` / `by_tier` under the hood; anything you
can build with it could also be hand-built with the raw API. Body
schema URIs are conventional (`annot://schema/song-section/v1`,
`lyric-line/v1`, `word/v1`) — pass `register_subtitle_schemas()` once
if you want Pydantic body validation.

## What’s in the core

```default
lacing/
├── time.py          RationalTime + TimeInterval — rational, half-open, never float
├── model.py         Annotation envelope + Reference union + Provenance (PROV-O subset)
├── tier.py          Tier + 5 ELAN tier stereotypes + constraint validator
├── allen.py         13 Allen relations + intersects + relate + composition
├── digest.py        annotation_value_digest — content digest for freshness / early cutoff
├── store/
│   ├── base.py      IntervalAnnotationStore (MutableMapping facade)
│   ├── memory.py    MemoryStore over `intervaltree`
│   ├── mapping.py   MappingStore — persisted to any injected MutableMapping (dict, dol JsonFiles, an app's store)
│   ├── sqlite.py    SqliteStore — persistent backend + .annot file format
│   └── postgres.py  PostgresStore — int8range + GiST + per-tier EXCLUDE
├── adapters/
│   ├── textgrid.py        Praat .TextGrid (interval + point tiers)
│   ├── webvtt.py          .vtt subtitles/captions
│   ├── web_annotation.py  W3C Web Annotation Data Model (JSON-LD)
│   ├── annot.py           .annot SQLite portable file format (lossless)
│   ├── eaf.py             ELAN EAF (4 stereotypes verbatim)
│   └── jams.py            JAMS (Music Information Retrieval) — namespaces → tiers
├── cli.py           `lacing` CLI: convert, query, validate, list-formats
├── quality.py       Cohen's κ, Krippendorff's α, interval IoU, boundary IoU
├── schema.py        Body schema registry + JSON Schema export + migrations
├── bodies/          Built-in body schemas (word, named-entity, ...)
└── server/          FastAPI HTTP server (Phase 2)
    ├── app.py           create_app(); ready-to-run `app` for uvicorn
    ├── deps.py          dependency-injection (store factory)
    ├── etag.py          ETag computation + If-Match parsing
    └── routers/         REST endpoints: annotations, tiers, adapters, meta
```

## Design rules in one breath

1. **Time is rational** — `RationalTime(value: int, rate: int)`. Wire format `{v, r}`. Never floats.
2. **Standoff** — annotations reference media by `(asset_id, interval)`; source is immutable.
3. **One envelope, typed body** — `Annotation.body: dict` validated by `body_schema_uri` (semver).
4. **Allen’s algebra is the public predicate API** — never write ad-hoc overlap checks.
5. **ELAN tier stereotypes verbatim** — `NONE`, `TIME_SUBDIVISION`, `INCLUDED_IN`, `SYMBOLIC_SUBDIVISION`, `SYMBOLIC_ASSOCIATION`.
6. **PROV-O provenance inline on every annotation** — `was_generated_by`, `was_attributed_to`, `was_derived_from`, `generated_at_time`. `generated_at_time` is wall-clock (`RationalTime.now()`); tick 0 is `UNKNOWN_GENERATED_AT`, never the epoch — consumers read it as unverifiable, not oldest.
7. **MIT/BSD/Apache licenses only.**

The full reasoning lives in [`misc/docs/`]() — four design docs
covering annotation systems generally, backend architecture, frontend UI,
and an OSS deep-dive of what to build on. The synthesized plan is in
[`misc/docs/Lacing Development Roadmap.md`]().

## Concrete recipes

### Build annotations programmatically

```python
from uuid import uuid4
from lacing import (
    Annotation,
    MediaRef,
    MemoryStore,
    Provenance,
    RationalTime,
    TimeInterval,
    Tier,
)

store = MemoryStore()
store.add_tier(Tier("words"))

store.add(
    Annotation(
        id=uuid4(),
        tier="words",
        reference=MediaRef(
            asset_id="blake3:abc123",
            interval=TimeInterval.from_seconds("0.0", "0.5", rate=1000),
        ),
        body={"text": "hello"},
        body_schema_uri="annot://schema/word/v1",
        provenance=Provenance(
            was_generated_by="user:thor",
            was_attributed_to="thor",
            generated_at_time=RationalTime.now(),
        ),
    )
)
```

### Query with Allen’s relations

```python
from lacing.allen import AllenRelation
from lacing.time import RationalTime, TimeInterval

w = TimeInterval(RationalTime(0, 1000), RationalTime(500, 1000))

list(store.intersects(w))  # any overlap
list(store.during(w))  # strictly inside w
list(store.contains(w))  # strictly contains w
list(store.relate(w, [AllenRelation.MEETS]))  # ends at w.start
```

### Persist annotations

```python
from lacing.store import SqliteStore

# Open or create a .annot file (SQLite under the hood)
store = SqliteStore("project.annot")
store.add_tier(...)
store.add(...)  # writes go straight to disk
store.set_meta("project", "demo")

# Same MutableMapping + Allen-relation interface as MemoryStore
for ann in store.intersects(window):
    ...
store.close()
```

The `.annot` file is the recommended portable handoff format — single-file
SQLite, Git-trackable, lossless round-trip with `MemoryStore`.

Already keep your data in a mapping (a `dict`, a `dol` `JsonFiles`, an app’s
store)? Hand it over and there is no second persistence path:

```python
from lacing.store import MappingStore

store = MappingStore(
    my_mapping
)  # one key per annotation id -> its JSON; tiers under one reserved key
store.add(...)  # writes through to my_mapping
store = MappingStore(my_mapping)  # reopening sees the same annotations and tiers
```

Single writer, no cross-process locking (unlike `SqliteStore`). Pass
`codec=JSON_BYTES_CODEC` for a mapping that stores bytes.

For multi-user / production scale, the same facade is available over
PostgreSQL:

```python
from lacing.store import PostgresStore
from lacing.tier import Tier

store = PostgresStore("postgresql://localhost/myproject", rate=1000)

# Per-tier non-overlap is enforced declaratively by the database — try to
# add an overlapping annotation in this tier and Postgres rejects the insert.
store.add_tier(Tier("speakers"), enforce_no_overlap=True)
```

The Postgres backend uses `int8range` + GiST (sub-millisecond overlap
queries at million-row scale) and exposes the same Allen-relation
methods. Times are normalized to a project-wide rate stored in `meta`.

### CLI

After `pip install -e .` the `lacing` command is on your PATH:

```bash
lacing list-formats                                          # show registered adapters
lacing convert speech.TextGrid speech.annot                  # convert between formats
lacing query speech.annot --start 1.0 --end 5.0 --rate 1000  # JSON-lines
lacing validate speech.annot                                 # parse + summary
```

### Body schemas, validation, migrations

Every annotation has a `body: dict` validated against the schema named by
its `body_schema_uri` (e.g., `annot://schema/named-entity/v2`). Register
your own with a Pydantic v2 model:

```python
from pydantic import BaseModel, Field
from lacing.schema import register_body_schema, register_migration, validate, migrate


class WordBodyV1(BaseModel):
    model_config = {"frozen": True, "extra": "forbid"}
    text: str = Field(...)
    speaker: str | None = None


register_body_schema("annot://schema/word/v1", WordBodyV1)

# Validate at runtime:
validate({"text": "hello"}, "annot://schema/word/v1")


# Register a forward migration v1 -> v2:
@register_migration(schema_name="word", from_version=1, to_version=2)
def _v1_to_v2(body: dict) -> dict:
    return {**body, "lemma": None}


# Migrate stored data:
migrated = migrate(
    {"text": "ran"}, from_uri="annot://schema/word/v1", to_uri="annot://schema/word/v2"
)
```

Export every registered schema to JSON Schema (the upstream for downstream
Zod codegen):

```python
from lacing.schema import export_json_schemas

export_json_schemas("./schema/")  # writes <name>/v<N>.json + index.json
```

Built-in body schemas live under `lacing/bodies/` (`word`, `named-entity`).
They register themselves on import.

### Run the HTTP server

```bash
pip install 'lacing[server]'
uvicorn lacing.server:app --reload
```

By default the server starts with an in-memory `SqliteStore`. Wire your
own backend (e.g., a `PostgresStore` or an `.annot` file) via FastAPI’s
dependency-override:

```python
from lacing.server import create_app
from lacing.server.deps import get_store
from lacing.store import SqliteStore

store = SqliteStore("project.annot", check_same_thread=False)
app = create_app()
app.dependency_overrides[get_store] = lambda: store
```

The REST surface (Phase 2.0):

```default
GET    /health
GET    /tiers                              list
POST   /tiers                              create or update
GET    /tiers/{name}                       get one
POST   /annotations                        create (returns ETag)
GET    /annotations                        list with optional ?tier&start&end&relation&rate
GET    /annotations/{id}                   get one (returns ETag)
PATCH  /annotations/{id}                   partial update; If-Match required
DELETE /annotations/{id}
POST   /import?format=webvtt               upload a file in any registered format
GET    /export?format=eaf                  dump store as a file
GET    /formats                            list registered adapters
GET    /schemas                            list registered body_schema_uris
GET    /schemas/{uri}                      JSON Schema for a URI
GET    /meta, PUT /meta/{key}              key/value metadata
GET    /oplog                              list mutations (filterable by clock)
GET    /oplog/latest-clock                 current Lamport clock value
GET    /state-at?clock=N                   replay log to clock N → snapshot
```

Every mutation gets a Lamport clock returned in the `X-Lacing-Clock`
response header. The op-log + `/state-at` endpoint give you full
time-travel debug — pick any past clock value and reconstruct exactly
what the system saw.

### MCP server — agents as first-class clients

```python
from lacing.oplog import InMemoryOpLog
from lacing.server.mcp import build_mcp_server
from lacing.store import SqliteStore

store = SqliteStore("project.annot", check_same_thread=False)
oplog = InMemoryOpLog()
server = build_mcp_server(store, oplog)
server.run()  # stdio transport by default
```

Tools registered (all take seconds — no need to construct rational-time
wire dicts): `add_annotation`, `query_annotations`, `get_annotation`,
`delete_annotation`, `accept_ai_suggestion`, `add_tier`, `list_tiers`,
`list_formats`, `latest_clock`, `state_at`. The MCP server shares the
same `store` + `oplog` as the FastAPI app, so a human edit via REST and
an agent edit via MCP land in the same op-log with the same Lamport
clock.

`accept_ai_suggestion` returns **two** records — `{"annotation": …, "review": …}` — because a review is two facts, not one. The reviewed
annotation keeps its `id` *and its provenance*: it was still generated
by the agent, so `was_generated_by` / `was_attributed_to` are left
alone and only `confidence` moves to 1.0 / 0.0. The human’s decision
becomes its own standoff `approval` review annotation on the `review`
tier, pointing at what it judged (`AnnotationRef`, **no interval**),
attributed to the reviewer and stamped with the review time. Rewriting
the agent’s provenance in place — which is what this used to do — erased
the one record proving the annotation was ever AI-generated (lacing#18).

The review carries no interval on purpose: a verdict about a whole
annotation has no position of its own on the media timeline, so it stays
out of untiered interval queries and out of every timeline export.
Giving it the reviewed annotation’s interval would put a blank cue at
that annotation’s exact timestamps in WebVTT, an empty interval in a
TextGrid tier, and an asset-less item in a Web Annotation collection —
one per accept.

### Freshness — “did the answer actually change?”

lacing has **three** digests answering three different questions. Picking the
wrong one is a silent correctness bug, so they are named apart:

| Digest                               | Over                      | Question                                                           |
|--------------------------------------|---------------------------|--------------------------------------------------------------------|
| `hash_bytes` / `hash_file`           | an artifact’s **bytes**   | *are these two files the same file?* (`Artifact.asset_id`)         |
| `lacing.server.etag.annotation_etag` | the **whole** annotation  | *has this record been touched since I read it?* (`If-Match` / 412) |
| `annotation_value_digest`            | an annotation’s **value** | *did the answer actually change?* (freshness / early cutoff)       |

A regeneration mints a fresh `id` and a fresh `provenance.generated_at_time`,
so `annotation_etag` changes even when the content is byte-identical. The value
digest excludes both — which is what lets a downstream freshness check key on
*upstream output values* rather than *upstream keys*, and stop propagating
invalidation when nothing actually changed.

```python
from lacing import annotation_value_digest, annotation_body_digest

# Same content, regenerated: same value digest, different id + timestamp.
assert annotation_value_digest(original) == annotation_value_digest(regenerated)

# Included in the value: body, body_schema_uri, tier, reference, confidence.
# Excluded: id, provenance.

# The narrow sibling — {body, body_schema_uri} only. It drops the ENTIRE
# reference (which asset, not just when) plus tier and confidence, so the same
# body over two DIFFERENT assets digests alike. Use it only when the consumer
# depends on nothing but what the annotation says.
assert annotation_body_digest(over_interview) == annotation_body_digest(over_broadcast)
```

A `body` must contain only JSON types. A non-`str` mapping key raises
`NonStringBodyKeyError` rather than digesting: JSON object keys are strings, so
`{1: "a", "1": "b"}` would collapse to `{"1": "b"}` and silently lose an entry
— two different annotations digesting alike. Within that contract the digest
never returns a wrong cache *hit*; it can return a spurious miss, which only
costs a recompute.

`lacing/digest.py` justifies each inclusion/exclusion in its docstring; read it
before changing the boundary, because changing it invalidates every consumer’s
cache at once.

### Inter-annotator agreement

```python
from lacing.quality import cohen_kappa, krippendorff_alpha, boundary_iou

# Two annotators on a categorical task
kappa = cohen_kappa(["A", "B", "A", "B"], ["A", "A", "A", "B"])

# Three annotators with missing data
alpha = krippendorff_alpha(
    [
        ["A", "B", None, "C"],
        ["A", "B", "B", "C"],
        ["A", "A", "B", "C"],
    ]
)

# Compare two segmentations
score = boundary_iou(
    [a.interval for a in store_a.by_tier("speakers")],
    [a.interval for a in store_b.by_tier("speakers")],
)
```

## License

MIT.

<p class="epythet-aggregates">This documentation as a single file: <a href="lacing.md">lacing.md</a> (Markdown, for agents).</p>


# _autosummary/lacing.adapters.annot.html.md

# lacing.adapters.annot

`.annot` portable file format adapter.

The `.annot` file is a SQLite database with the schema defined in
`lacing.store.sqlite`. It’s the recommended portable handoff and archive
format (BACK-DOC §3.1: “SQLite-as-app-format” — Git-trackable,
email-attachable, single-file).

Unlike text-based adapters (TextGrid, WebVTT, JSON-LD), this one is
non-lossy: the full annotation envelope, references, body, body schema URI,
provenance, and confidence all round-trip exactly.

When loading, returns an in-memory `MemoryStore` for compatibility with
the rest of the adapter API. To open an `.annot` file as a *persistent*
store you can mutate, use `SqliteStore(path)` directly:

```pycon
>>> from lacing.store import SqliteStore
>>> store = SqliteStore("project.annot")  # writes go straight to disk
```

### Functions

| [`dump`](_autosummary/lacing.adapters.annot.html.md#lacing.adapters.annot.dump)(store[, target, overwrite])        | Write `store` as an `.annot` SQLite file.   |
|------------------------------------------------------------------------------------------|---------------------------------------------|
| [`load`](_autosummary/lacing.adapters.annot.html.md#lacing.adapters.annot.load)(source, \*[, persistent, migrate]) | Open an `.annot` file.                      |

### lacing.adapters.annot.dump(store, target=None, , overwrite=True, \*\*\_kwargs)

Write `store` as an `.annot` SQLite file.

* **Parameters:**
  * **store** ([`IntervalAnnotationStore`](_autosummary/lacing.store.base.html.md#lacing.store.base.IntervalAnnotationStore)) – Source store. Can be any IntervalAnnotationStore.
  * **target** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Output path. None = return bytes.
  * **overwrite** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True (default), replace any existing file at `target`.
    If False and the file exists, raise `FileExistsError`.
* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### lacing.adapters.annot.load(source, , persistent=False, migrate=False, \*\*\_kwargs)

Open an `.annot` file.

* **Parameters:**
  * **source** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike)) – Path to an `.annot` file. Bytes input is supported by
    writing to a temp file first.
  * **persistent** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, return an open `SqliteStore` (writes go to
    the file). If False (default), return a `MemoryStore` snapshot.
  * **migrate** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Opt-in to upgrading a file written at an older store
    `schema_version` on open (see [`lacing.store.migrations`](_autosummary/lacing.store.migrations.html.md#module-lacing.store.migrations)).
    A stale file otherwise refuses with
    [`SchemaMismatchError`](_autosummary/lacing.store.sqlite.html.md#lacing.store.sqlite.SchemaMismatchError) — same
    contract as `SqliteStore`. For **bytes** input the copy in
    the temp file is migrated regardless: it is not the caller’s
    file, and refusing to read bytes that cannot be re-pointed at
    the migration CLI would be a dead end.
* **Return type:**
  [`IntervalAnnotationStore`](_autosummary/lacing.store.base.html.md#lacing.store.base.IntervalAnnotationStore)
* **Returns:**
  `MemoryStore` (default) or `SqliteStore` (if `persistent=True`).


# _autosummary/lacing.adapters.eaf.html.md

# lacing.adapters.eaf

ELAN EAF adapter (Max Planck Institute’s annotation tool format).

EAF is the canonical format for tier-hierarchy annotation. Every Phase 0
adapter modeled flat tiers; this is the first that exercises lacing’s
stereotype constraint model end-to-end (TIME_SUBDIVISION, INCLUDED_IN,
SYMBOLIC_SUBDIVISION, SYMBOLIC_ASSOCIATION). See ANN-DOC §B and OSS-DOC
tier-2.4.

## Mapping

EAF                                    ↔ lacing
`TIER_ID`                            ↔ `Tier.name`
`PARENT_REF`                         ↔ `Tier.parent`
`LINGUISTIC_TYPE_REF` + `CONSTRAINTS` ↔ `Tier.stereotype`
`ALIGNABLE_ANNOTATION`               ↔ `Annotation` with `MediaRef`
`ANNOTATION_VALUE`                   ↔ `body['text']`
`MEDIA_DESCRIPTOR / MEDIA_URL`       ↔ `MediaRef.asset_id` (when present)

## Time

EAF stores ALL times as integer milliseconds (the format requires it).
We map directly to `RationalTime(ms, rate=1000)` — exact, no floats.
Pass `rate=` to re-quantize to a different project rate (must be
exact; raises `LossyTimeConversionError` otherwise).

## Lossy on dump

- ELAN provenance is at the document level (`AUTHOR`, `DATE`); per-
  annotation provenance and confidence are dropped.
- `REF_ANNOTATION` (annotations referencing parent annotations rather
  than time slots) is **not yet supported**; the loader emits
  `ALIGNABLE_ANNOTATION` data only. Round-trip of REF annotations is
  Phase 2 work.
- `CV_REF` controlled vocabularies are not preserved.
- The ELAN `default` empty tier and `default-lt` linguistic type
  (auto-created by pympi) are filtered out on load if they have no
  annotations.

Spec: [https://www.mpi.nl/tools/elan/EAFv3.0.xsd](https://www.mpi.nl/tools/elan/EAFv3.0.xsd)

### Functions

| [`dump`](_autosummary/lacing.adapters.eaf.html.md#lacing.adapters.eaf.dump)(store[, target, pretty, media_url])   | Write annotations as an ELAN EAF file.      |
|---------------------------------------------------------------------------------------------|---------------------------------------------|
| [`load`](_autosummary/lacing.adapters.eaf.html.md#lacing.adapters.eaf.load)(source, \*[, rate, asset_id, ...])    | Load an ELAN EAF file into a `MemoryStore`. |

### lacing.adapters.eaf.dump(store, target=None, , pretty=True, media_url=None, \*\*\_kwargs)

Write annotations as an ELAN EAF file.

* **Parameters:**
  * **store** ([`IntervalAnnotationStore`](_autosummary/lacing.store.base.html.md#lacing.store.base.IntervalAnnotationStore)) – Source store. Only annotations with `MediaRef` and a
    non-None interval are exported.
  * **target** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Output path. None = return bytes.
  * **pretty** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Pretty-print XML (default True).
  * **media_url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Optional `MEDIA_URL` for `MEDIA_DESCRIPTOR`. If
    None and the store has annotations, use the first MediaRef’s
    `asset_id`.
* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### lacing.adapters.eaf.load(source, , rate=24000, asset_id=None, attribution='anonymous', drop_default_tier=True, \*\*\_kwargs)

Load an ELAN EAF file into a `MemoryStore`.

* **Parameters:**
  * **source** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike)) – Path or bytes containing EAF XML.
  * **rate** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Quantization rate (default `DEFAULT_RATE`). EAF times are
    milliseconds; conversions must be exact.
  * **asset_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Override media reference. None = use the first
    `MEDIA_DESCRIPTOR/MEDIA_URL` from the EAF, or the default.
  * **attribution** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – `Provenance.was_attributed_to` value.
  * **drop_default_tier** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True (default), filter out the empty
    `default` tier auto-created by pympi.
* **Return type:**
  [`IntervalAnnotationStore`](_autosummary/lacing.store.base.html.md#lacing.store.base.IntervalAnnotationStore)


# _autosummary/lacing.adapters.html.md

# lacing.adapters

I/O adapter registry.

The core never imports a format module. Each adapter registers itself by
calling [`register_adapter()`](_autosummary/lacing.adapters.html.md#lacing.adapters.register_adapter) at import time. Users opt in by importing
the adapter module:

> from lacing.adapters import textgrid  # noqa: F401  — registers itself

Or by using the convenience top-level loaders that look up by extension or
`media_type`.

### Functions

| [`dump`](_autosummary/lacing.adapters.html.md#lacing.adapters.dump)(store[, target])                         | Convenience: serialize `store` via the named adapter.          |
|------------------------------------------------------------------------------------------------|----------------------------------------------------------------|
| [`find_adapter`](_autosummary/lacing.adapters.html.md#lacing.adapters.find_adapter)(\*[, extension, media_type])     | Find an adapter by extension (case-insensitive) or media type. |
| [`get_adapter`](_autosummary/lacing.adapters.html.md#lacing.adapters.get_adapter)(name)                             | Look up an adapter by name.                                    |
| [`load`](_autosummary/lacing.adapters.html.md#lacing.adapters.load)(source, \*[, format])                    | Convenience: dispatch `source` to the right adapter.           |
| [`register_adapter`](_autosummary/lacing.adapters.html.md#lacing.adapters.register_adapter)(\*, name, load, dump[, ...]) | Register an adapter.                                           |
| [`registered`](_autosummary/lacing.adapters.html.md#lacing.adapters.registered)()                                  | All currently registered adapters (in registration order).     |

### Classes

| [`AdapterSpec`](_autosummary/lacing.adapters.html.md#lacing.adapters.AdapterSpec)(name, extensions, media_types, ...)   | Registered adapter for one format.   |
|----------------------------------------------------------------------------------------------------|--------------------------------------|

### *class* lacing.adapters.AdapterSpec(name, extensions, media_types, load, dump, body_schema_uris=<factory>, description='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Registered adapter for one format.

### lacing.adapters.dump(store, target=None, , format, \*\*kwargs)

Convenience: serialize `store` via the named adapter.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### lacing.adapters.find_adapter(, extension=None, media_type=None)

Find an adapter by extension (case-insensitive) or media type.

* **Return type:**
  [`AdapterSpec`](_autosummary/lacing.adapters.html.md#lacing.adapters.AdapterSpec) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### lacing.adapters.get_adapter(name)

Look up an adapter by name. Raises `KeyError` if missing.

* **Return type:**
  [`AdapterSpec`](_autosummary/lacing.adapters.html.md#lacing.adapters.AdapterSpec)

### lacing.adapters.load(source, , format=None, \*\*kwargs)

Convenience: dispatch `source` to the right adapter.

If `format` is given, looks up by name. Otherwise, if `source` is a
path, infers from extension. Raises `ValueError` if it can’t pick.

* **Return type:**
  [`IntervalAnnotationStore`](_autosummary/lacing.store.base.html.md#lacing.store.base.IntervalAnnotationStore)

### lacing.adapters.register_adapter(, name, load, dump, extensions=(), media_types=(), body_schema_uris=(), description='')

Register an adapter. Idempotent: re-registering the same name replaces.

* **Return type:**
  [`AdapterSpec`](_autosummary/lacing.adapters.html.md#lacing.adapters.AdapterSpec)

### lacing.adapters.registered()

All currently registered adapters (in registration order).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`AdapterSpec`](_autosummary/lacing.adapters.html.md#lacing.adapters.AdapterSpec)]

### Modules

| [`annot`](_autosummary/lacing.adapters.annot.html.md#module-lacing.adapters.annot)                   | `.annot` portable file format adapter.                            |
|-------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------|
| [`eaf`](_autosummary/lacing.adapters.eaf.html.md#module-lacing.adapters.eaf)                       | ELAN EAF adapter (Max Planck Institute's annotation tool format). |
| [`jams`](_autosummary/lacing.adapters.jams.html.md#module-lacing.adapters.jams)                     | JAMS (JSON Annotated Music Specification) adapter.                |
| [`label_studio`](_autosummary/lacing.adapters.label_studio.html.md#module-lacing.adapters.label_studio)     | Label Studio JSON adapter.                                        |
| [`otio`](_autosummary/lacing.adapters.otio.html.md#module-lacing.adapters.otio)                     | OpenTimelineIO (OTIO) adapter — NLE / video editing interchange.  |
| [`textgrid`](_autosummary/lacing.adapters.textgrid.html.md#module-lacing.adapters.textgrid)             | Praat TextGrid adapter.                                           |
| [`web_annotation`](_autosummary/lacing.adapters.web_annotation.html.md#module-lacing.adapters.web_annotation) | W3C Web Annotation Data Model adapter (JSON-LD).                  |
| [`webvtt`](_autosummary/lacing.adapters.webvtt.html.md#module-lacing.adapters.webvtt)                 | WebVTT (W3C TimedText) adapter.                                   |


# _autosummary/lacing.adapters.jams.html.md

# lacing.adapters.jams

JAMS (JSON Annotated Music Specification) adapter.

JAMS is the standard MIR (Music Information Retrieval) annotation format.
Each `Annotation` carries a *namespace* (`chord`, `beat`, `key`,
`segment_open`, `tag_open`, …) that defines the type of its
observations. We map JAMS namespaces to lacing **tiers**, observations to
`Annotation` rows, and each observation’s typed `value` into the lacing
body envelope.

## Mapping

JAMS                                ↔ lacing
`Annotation.namespace`            ↔ `Tier.name`
`Observation.time, .duration`     ↔ `MediaRef.interval` (half-open)
`Observation.value`               ↔ `body['value']` (namespace-typed)
`Observation.confidence`          ↔ `Annotation.confidence`
`annotation_metadata.annotator`   ↔ `Provenance.was_attributed_to`
`file_metadata.identifiers/title` ↔ `MediaRef.asset_id`

## Body schema

We use a single generic body schema URI:

> annot://schema/jams-observation/v1

with shape `{"value": <namespace-typed>, "namespace": <str>}`. Registering
namespace-specific Pydantic body schemas (one per JAMS namespace) is left to
downstream packages — JAMS namespaces are extensible and we don’t want the
adapter to impose a fixed catalog.

## Time

JAMS times are float seconds. We convert at the parse boundary via
`Fraction(repr(...))` to recover exact rational values. Default rate is
`DEFAULT_RATE` (24000); pass `rate=` to use a different project rate.
`LossyTimeConversionError` is raised if any observation can’t be exactly
represented.

## Lossy on dump

- Per-annotation provenance beyond `annotator` (`was_derived_from`,
  `was_generated_by`, `activity`) is dropped — JAMS has no
  corresponding fields.
- `body['note']` and other extra body fields are dropped: only
  `body['value']` round-trips.
- File-level `identifiers` and `title` are populated from the first
  annotation’s `MediaRef.asset_id` if available.

Spec: [https://jams.readthedocs.io/](https://jams.readthedocs.io/)

### Functions

| [`dump`](_autosummary/lacing.adapters.jams.html.md#lacing.adapters.jams.dump)(store[, target, title, artist, duration])   | Serialize `store` as a JAMS file.               |
|---------------------------------------------------------------------------------------------------|-------------------------------------------------|
| [`load`](_autosummary/lacing.adapters.jams.html.md#lacing.adapters.jams.load)(source, \*[, rate, asset_id, attribution])  | Load a JAMS file or bytes into a `MemoryStore`. |

### lacing.adapters.jams.dump(store, target=None, , title='', artist='', duration=None, \*\*\_kwargs)

Serialize `store` as a JAMS file.

* **Parameters:**
  * **store** ([`IntervalAnnotationStore`](_autosummary/lacing.store.base.html.md#lacing.store.base.IntervalAnnotationStore)) – Source store. Only annotations with a `MediaRef` interval
    and a `body['value']` are exported.
  * **target** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Output path. None = return bytes.
  * **title** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – `file_metadata.title` value.
  * **artist** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – `file_metadata.artist` value.
  * **duration** ([`float`](https://docs.python.org/3/builtins/functions.html#float) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – `file_metadata.duration` (seconds). If None, computed
    as the maximum end-time across all annotations.
* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### lacing.adapters.jams.load(source, , rate=24000, asset_id=None, attribution=None, \*\*\_kwargs)

Load a JAMS file or bytes into a `MemoryStore`.

* **Parameters:**
  * **source** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike)) – Path or bytes containing JAMS JSON.
  * **rate** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Quantization rate.
  * **asset_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Override media reference. None = derive from
    `file_metadata.identifiers` or `title`.
  * **attribution** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Override per-annotation attribution. None = use each
    annotation’s own annotator/curator.
* **Return type:**
  [`IntervalAnnotationStore`](_autosummary/lacing.store.base.html.md#lacing.store.base.IntervalAnnotationStore)


# _autosummary/lacing.adapters.label_studio.html.md

# lacing.adapters.label_studio

Label Studio JSON adapter.

Label Studio is a popular labeling tool whose canonical export shape is
a JSON array of *tasks*, each with one or more *annotations* containing
typed *results*. We focus on time-interval results (the `Audio`,
`Video`, and `TimeSeries` labeling controls) since lacing is an
interval-annotation system.

## Mapping

Label Studio                                ↔ lacing
`data.audio` / `data.video` / `data.url` ↔ `MediaRef.asset_id`
`result.from_name`                        ↔ `Tier.name`
`result.value.{start, end}` (seconds)     ↔ `MediaRef.interval`
`result.value.labels` (list of strings)   ↔ `body['labels']`
`annotation.completed_by` (or `user`)   ↔ `Provenance.was_attributed_to`
`result.origin` (“prediction”/…)        → `confidence` heuristic
`result.id`                               → preserved in body
`result.type`                             → preserved in body

## Lossy on dump

- Per-result Label Studio metadata (`image_rotation`,
  `original_width`, etc.) is dropped — they’re irrelevant for time
  intervals.
- Predictions vs annotations distinction is collapsed: lacing tracks
  AI vs human via `confidence` and `provenance.was_generated_by`,
  and we set Label Studio’s `origin` to `manual` on dump.
- Multi-annotator support: only one annotation per task is emitted on
  dump (the first), since lacing’s per-annotation provenance fits
  Label Studio’s per-task `annotations` list awkwardly.

Spec: [https://labelstud.io/guide/export.html](https://labelstud.io/guide/export.html)

### Functions

| [`dump`](_autosummary/lacing.adapters.label_studio.html.md#lacing.adapters.label_studio.dump)(store[, target, pretty])                   | Serialize `store` as a Label Studio JSON export.      |
|--------------------------------------------------------------------------------------------------|-------------------------------------------------------|
| [`load`](_autosummary/lacing.adapters.label_studio.html.md#lacing.adapters.label_studio.load)(source, \*[, rate, asset_id, attribution]) | Load a Label Studio JSON export into a `MemoryStore`. |

### lacing.adapters.label_studio.dump(store, target=None, , pretty=True, \*\*\_kwargs)

Serialize `store` as a Label Studio JSON export.

Annotations are grouped by `MediaRef.asset_id` — each asset becomes
one task with a single `annotations[0]` entry collecting every
region. `confidence < 0.5` flips the result into Label Studio’s
`predictions` slot rather than `annotations`.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### lacing.adapters.label_studio.load(source, , rate=24000, asset_id=None, attribution=None, \*\*\_kwargs)

Load a Label Studio JSON export into a `MemoryStore`.

* **Parameters:**
  * **source** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike)) – Path, bytes, or JSON string. Accepts both an array of
    tasks and a single task dict.
  * **rate** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Quantization rate.
  * **asset_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Override the resolved asset for every annotation.
  * **attribution** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Override per-annotation attribution.
* **Return type:**
  [`IntervalAnnotationStore`](_autosummary/lacing.store.base.html.md#lacing.store.base.IntervalAnnotationStore)


# _autosummary/lacing.adapters.otio.html.md

# lacing.adapters.otio

OpenTimelineIO (OTIO) adapter — NLE / video editing interchange.

OTIO is the Academy Software Foundation’s standard for video timeline
interchange. Per OSS-DOC, lacing’s `RationalTime` design is directly
inspired by OTIO’s — this adapter is the place where that parity pays
off.

## Mapping

OTIO                                     ↔ lacing
`Timeline`                             ↔ a project (one OTIO file = one store)
`Track`                                ↔ a tier (track `name` becomes tier name)
`Clip`                                 ↔ `Annotation` (interval = source_range

> offset onto the track timeline)

`Clip.media_reference.target_url`      ↔ `MediaRef.asset_id`
`Marker` (on Track or Clip)            ↔ point `Annotation`
`Clip.metadata` / `Marker.metadata`  ↔ `body['otio_metadata']`
`MarkerColor`                          ↔ `body['color']`

## Time

OTIO `RationalTime` maps to lacing `RationalTime` directly — same
`(value, rate)` shape. We re-quantize to the requested project rate
on load if needed; raises `LossyTimeConversionError` on inexact
conversions.

## Lossy on dump

- Per-annotation provenance (`was_attributed_to`, `was_derived_from`)
  is dropped — OTIO carries no per-clip annotator field.
- Confidence is dropped — OTIO has no equivalent.
- Track effects + transitions are dropped — out of lacing’s scope.
- Clips are emitted with an `ExternalReference` whose `target_url`
  is the `MediaRef.asset_id`.

Spec: [https://opentimelineio.readthedocs.io/](https://opentimelineio.readthedocs.io/)

### Functions

| [`dump`](_autosummary/lacing.adapters.otio.html.md#lacing.adapters.otio.dump)(store[, target, name])                     | Serialize `store` as an OTIO file.           |
|--------------------------------------------------------------------------------------------------|----------------------------------------------|
| [`load`](_autosummary/lacing.adapters.otio.html.md#lacing.adapters.otio.load)(source, \*[, rate, asset_id, attribution]) | Load an OTIO file/JSON into a `MemoryStore`. |

### lacing.adapters.otio.dump(store, target=None, , name='lacing', \*\*\_kwargs)

Serialize `store` as an OTIO file.

Each tier becomes one Track. Annotations whose body has
`kind=='marker'` are emitted as `Marker``s on the track at their
interval start. Annotations whose body has ``kind=='clip'` (or no
explicit kind) become Clips with `ExternalReference` to the
`asset_id` and `source_range` matching the lacing interval.

#### NOTE
lacing intervals are absolute positions; OTIO clips are
sequential within a track. We sort + place clips back-to-back per
tier; gaps between intervals become explicit `Gap` items.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### lacing.adapters.otio.load(source, , rate=24000, asset_id=None, attribution='anonymous', \*\*\_kwargs)

Load an OTIO file/JSON into a `MemoryStore`.

Each track becomes a tier. Each clip becomes one `Annotation` with
`MediaRef` pointing at the clip’s media reference (or the
`asset_id` override). Markers on the timeline, track, or clip
become point `Annotation``s on a ``markers` tier.

* **Parameters:**
  * **source** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike)) – Path or bytes containing OTIO JSON.
  * **rate** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Quantization rate.
  * **asset_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Override media reference for every clip. None = use
    each clip’s own `media_reference.target_url`.
  * **attribution** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – `Provenance.was_attributed_to` value.
* **Return type:**
  [`IntervalAnnotationStore`](_autosummary/lacing.store.base.html.md#lacing.store.base.IntervalAnnotationStore)


# _autosummary/lacing.adapters.textgrid.html.md

# lacing.adapters.textgrid

Praat TextGrid adapter.

Praat TextGrids have two tier types: `IntervalTier` (ranges) and
`PointTier` (instants — Praat’s own `TextTier`). lacing maps both to a
single `Annotation` envelope; point annotations use a zero-length
`TimeInterval` (`start == end`). See ANN-DOC §B and the
`lacing-adapter-authoring` skill.

Lossy fields:
: Praat has no provenance, no schema URIs, and no confidence. On load we
  synthesize provenance with `was_generated_by="adapter:textgrid"`.
  On dump we drop everything but `tier`, `interval` (or point), and
  `body['text']` (or the raw `body` repr if no `text` key).

Time discipline:
: Praat stores times as ASCII decimal strings — these can be exact at
  common rates. We convert through `Fraction(str)` to avoid float
  ingestion. Default rate is `DEFAULT_RATE` (24000); pass `rate=` to
  override.

Tier stereotype mapping:
: ELAN tier stereotypes have no Praat equivalent. Every loaded tier gets
  stereotype `NONE`. On dump, only the `tier` *name* is preserved.

### Functions

| [`dump`](_autosummary/lacing.adapters.textgrid.html.md#lacing.adapters.textgrid.dump)(store[, target, format, include_empty])   | Write a `IntervalAnnotationStore` as a Praat TextGrid.   |
|-------------------------------------------------------------------------------------------------|----------------------------------------------------------|
| [`load`](_autosummary/lacing.adapters.textgrid.html.md#lacing.adapters.textgrid.load)(source, \*[, rate, asset_id, ...])        | Load a TextGrid file or bytes into a `MemoryStore`.      |

### lacing.adapters.textgrid.dump(store, target=None, , format='long_textgrid', include_empty=True, \*\*\_kwargs)

Write a `IntervalAnnotationStore` as a Praat TextGrid.

* **Parameters:**
  * **store** ([`IntervalAnnotationStore`](_autosummary/lacing.store.base.html.md#lacing.store.base.IntervalAnnotationStore)) – Source store. Only annotations with a `MediaRef` interval
    are exported; timeless (`AnnotationRef` without sub-interval)
    annotations are silently dropped.
  * **target** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Output path. If None, returns bytes.
  * **format** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – One of praatio’s formats: `"long_textgrid"`,
    `"short_textgrid"`, `"json"`.
  * **include_empty** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to emit empty-label intervals on point tiers.
* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### lacing.adapters.textgrid.load(source, , rate=24000, asset_id='textgrid:unspecified', attribution='anonymous', include_empty=True, \*\*\_kwargs)

Load a TextGrid file or bytes into a `MemoryStore`.

* **Parameters:**
  * **source** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike)) – Path or bytes containing TextGrid content.
  * **rate** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Quantization rate for converted times.
  * **asset_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – `MediaRef.asset_id` to attach to every annotation.
  * **attribution** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – `Provenance.was_attributed_to` value.
  * **include_empty** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If False, drop intervals/points with empty labels.
* **Return type:**
  [`IntervalAnnotationStore`](_autosummary/lacing.store.base.html.md#lacing.store.base.IntervalAnnotationStore)


# _autosummary/lacing.adapters.web_annotation.html.md

# lacing.adapters.web_annotation

W3C Web Annotation Data Model adapter (JSON-LD).

W3C Web Annotation is the closest thing to a universal annotation standard
[ANN-DOC §A]. Each annotation is a JSON-LD object with `body`, `target`,
`creator`, `created`, and a `motivation`.

We map W3C annotations onto lacing’s envelope as follows:

> target.source                   -> MediaRef.asset_id
> target.selector (FragmentSelector with t=…) -> MediaRef.interval (or point)
> body                            -> Annotation.body[‘body’] (passthrough)
> motivation                      -> Annotation.body[‘motivation’]
> creator (string or dict)        -> Provenance.was_attributed_to
> created (xsd:dateTime ISO 8601) -> preserved in body[‘created’] as string

> > (cannot be exact in RationalTime)

> Annotation.tier                 -> body[‘tier’] OR mapped from motivation

We support the **Media Fragment URI** time selector `#t=<start>,<end>` per
W3C Media Fragments spec ([https://www.w3.org/TR/media-frags/](https://www.w3.org/TR/media-frags/)). Times are
parsed as decimal seconds via `Fraction(str)`.

Lossy fields:

> - W3C `created` is xsd:dateTime — kept as a string in the body, not
>   mapped to `Provenance.generated_at_time`.
> - Multi-target annotations: only the first target is honored.
> - Selectors other than FragmentSelector with `t=` and TextQuoteSelector
>   are preserved verbatim in body[‘selector’] but not interpreted.

Spec: [https://www.w3.org/TR/annotation-model/](https://www.w3.org/TR/annotation-model/)

### Functions

| [`dump`](_autosummary/lacing.adapters.web_annotation.html.md#lacing.adapters.web_annotation.dump)(store[, target, as_collection, pretty])   | Serialize `store` as W3C Web Annotation JSON-LD.         |
|-------------------------------------------------------------------------------------------------|----------------------------------------------------------|
| [`load`](_autosummary/lacing.adapters.web_annotation.html.md#lacing.adapters.web_annotation.load)(source, \*[, rate, asset_id, tier])       | Load a W3C Web Annotation document into a `MemoryStore`. |

### lacing.adapters.web_annotation.dump(store, target=None, , as_collection=True, pretty=True, \*\*\_kwargs)

Serialize `store` as W3C Web Annotation JSON-LD.

* **Parameters:**
  * **store** ([`IntervalAnnotationStore`](_autosummary/lacing.store.base.html.md#lacing.store.base.IntervalAnnotationStore)) – Source store. Only annotations with a `MediaRef` interval
    are exported.
  * **target** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Output path. None = return bytes.
  * **as_collection** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, wrap output as an `AnnotationCollection`
    with an `items` array. If False and the store has a single
    annotation, emit it bare.
  * **pretty** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Pretty-print with 2-space indent.
* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### lacing.adapters.web_annotation.load(source, , rate=1000, asset_id=None, tier='annotations', \*\*\_kwargs)

Load a W3C Web Annotation document into a `MemoryStore`.

Accepts a single annotation, an AnnotationCollection/AnnotationPage
(`items`), or a JSON list at the top level.

* **Parameters:**
  * **source** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike)) – Path, bytes, or JSON string.
  * **rate** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Quantization rate.
  * **asset_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Override target source for all annotations. None = use
    each annotation’s own `target.source`.
  * **tier** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Tier name to assign annotations that have no explicit
    `body['tier']` value.
* **Return type:**
  [`IntervalAnnotationStore`](_autosummary/lacing.store.base.html.md#lacing.store.base.IntervalAnnotationStore)


# _autosummary/lacing.adapters.webvtt.html.md

# lacing.adapters.webvtt

WebVTT (W3C TimedText) adapter.

WebVTT is a flat caption format: one stream of cues, no tier hierarchy.
Cues have a start time, an end time, optional id and settings, and a payload
(text with limited inline markup).

We map cues to a single tier named `cues` (override with `tier=`).
Each cue becomes one `Annotation` whose body holds:

> {“text”: str, “id”: str | None, “settings”: dict[str, str]}

Lossy fields on dump:
: Provenance, confidence, schema URIs, and tier metadata are dropped.
  Only the text, id (if present), and settings are emitted.

Time discipline:
: WebVTT timestamps are `HH:MM:SS.mmm` or `MM:SS.mmm` (with milliseconds).
  Parsing goes through string fractions to avoid float ingestion. Default
  rate is 1000 (millisecond precision); override with `rate=` if your
  project uses a different canonical rate.

Spec: [https://www.w3.org/TR/webvtt1/](https://www.w3.org/TR/webvtt1/)

### Functions

| [`dump`](_autosummary/lacing.adapters.webvtt.html.md#lacing.adapters.webvtt.dump)(store[, target, tier])             | Write annotations as WebVTT.                              |
|------------------------------------------------------------------------------------------|-----------------------------------------------------------|
| [`load`](_autosummary/lacing.adapters.webvtt.html.md#lacing.adapters.webvtt.load)(source, \*[, rate, asset_id, ...]) | Parse a WebVTT file or string/bytes into a `MemoryStore`. |

### lacing.adapters.webvtt.dump(store, target=None, , tier=None, \*\*\_kwargs)

Write annotations as WebVTT.

* **Parameters:**
  * **store** ([`IntervalAnnotationStore`](_autosummary/lacing.store.base.html.md#lacing.store.base.IntervalAnnotationStore)) – Source store. Only annotations from `tier` (default: all
    tiers) with a `MediaRef` interval are emitted, sorted by start.
  * **target** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Output path. If None, returns bytes (UTF-8).
  * **tier** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Restrict export to this tier. None = export all annotations.
* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### lacing.adapters.webvtt.load(source, , rate=1000, asset_id='webvtt:unspecified', attribution='anonymous', tier='cues', \*\*\_kwargs)

Parse a WebVTT file or string/bytes into a `MemoryStore`.

* **Return type:**
  [`IntervalAnnotationStore`](_autosummary/lacing.store.base.html.md#lacing.store.base.IntervalAnnotationStore)


# _autosummary/lacing.allen.html.md

# lacing.allen

Allen’s 13 interval relations as pure predicates.

Public predicate API — never write ad-hoc overlap checks elsewhere in lacing.
See ANN-DOC §A and `.claude/skills/lacing-time-and-intervals/SKILL.md`.

Convention: intervals are half-open `[start, end)`. Boundary cases for
`meets`/`met_by` use exact equality on rational time. Half-open
intervals share zero measure at a boundary, so `meets` is NOT an
intersection — see [`intersects()`](_autosummary/lacing.allen.html.md#lacing.allen.intersects).

Relation table (a R b means: a’s relation to b is R):

| Relation      | Sym.   | Predicate (with intervals a, b)       |
|---------------|--------|---------------------------------------|
| before        | <      | a.end < b.start                       |
| after         | >      | a.start > b.end                       |
| meets         | m      | a.end == b.start                      |
| met_by        | mi     | a.start == b.end                      |
| overlaps      | o      | a.start < b.start < a.end < b.end     |
| overlapped_by | oi     | b.start < a.start < b.end < a.end     |
| starts        | s      | a.start == b.start and a.end < b.end  |
| started_by    | si     | a.start == b.start and a.end > b.end  |
| during        | d      | a.start > b.start and a.end < b.end   |
| contains      | di     | a.start < b.start and a.end > b.end   |
| finishes      | f      | a.start > b.start and a.end == b.end  |
| finished_by   | fi     | a.start < b.start and a.end == b.end  |
| equals        | =      | a.start == b.start and a.end == b.end |

### Module Attributes

| [`PREDICATE_BY_RELATION`](_autosummary/lacing.allen.html.md#lacing.allen.PREDICATE_BY_RELATION)   | Lookup table — used by `IntervalAnnotationStore` to dispatch by relation.   |
|--------------------------------------------------------------------------|-----------------------------------------------------------------------------|

### Functions

| `after`(a, b)                                                     |                                                                     |
|-------------------------------------------------------------------|---------------------------------------------------------------------|
| `before`(a, b)                                                    |                                                                     |
| [`compose`](_autosummary/lacing.allen.html.md#lacing.allen.compose)(r1, r2)  | Allen's composition.                                                |
| `contains`(a, b)                                                  |                                                                     |
| `during`(a, b)                                                    |                                                                     |
| `equals`(a, b)                                                    |                                                                     |
| `finished_by`(a, b)                                               |                                                                     |
| `finishes`(a, b)                                                  |                                                                     |
| [`intersects`](_autosummary/lacing.allen.html.md#lacing.allen.intersects)(a, b) | True iff `a` and `b` share any positive measure of time.            |
| `meets`(a, b)                                                     |                                                                     |
| `met_by`(a, b)                                                    |                                                                     |
| `overlapped_by`(a, b)                                             |                                                                     |
| `overlaps`(a, b)                                                  |                                                                     |
| [`relate`](_autosummary/lacing.allen.html.md#lacing.allen.relate)(a, b)     | Return the unique Allen relation between two well-formed intervals. |
| `started_by`(a, b)                                                |                                                                     |
| `starts`(a, b)                                                    |                                                                     |

### Classes

| [`AllenRelation`](_autosummary/lacing.allen.html.md#lacing.allen.AllenRelation)(\*values)   | The thirteen Allen relations.   |
|----------------------------------------------------------------------------|---------------------------------|

### *class* lacing.allen.AllenRelation(\*values)

Bases: [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Enum`](https://docs.python.org/3/library/enum.html#enum.Enum)

The thirteen Allen relations.

Symbols match Allen (1983); inverse pairs end in `i`.

#### inverse()

The inverse relation.

* **Return type:**
  [`AllenRelation`](_autosummary/lacing.allen.html.md#lacing.allen.AllenRelation)

### lacing.allen.PREDICATE_BY_RELATION *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[AllenRelation](_autosummary/lacing.allen.html.md#lacing.allen.AllenRelation), [Callable](https://docs.python.org/3/library/typing.html#typing.Callable)[[[TimeInterval](_autosummary/lacing.time.html.md#lacing.time.TimeInterval), [TimeInterval](_autosummary/lacing.time.html.md#lacing.time.TimeInterval)], [bool](https://docs.python.org/3/builtins/functions.html#bool)]]* *= {AllenRelation.BEFORE: <function before>, AllenRelation.EQUALS: <function equals>, AllenRelation.AFTER: <function after>, AllenRelation.DURING: <function during>, AllenRelation.CONTAINS: <function contains>, AllenRelation.FINISHES: <function finishes>, AllenRelation.FINISHED_BY: <function finished_by>, AllenRelation.MEETS: <function meets>, AllenRelation.MET_BY: <function met_by>, AllenRelation.OVERLAPS: <function overlaps>, AllenRelation.OVERLAPPED_BY: <function overlapped_by>, AllenRelation.STARTS: <function starts>, AllenRelation.STARTED_BY: <function started_by>}*

Lookup table — used by `IntervalAnnotationStore` to dispatch by relation.

### lacing.allen.compose(r1, r2)

Allen’s composition. See module docstring.

Phase 0 returns the universal set (all 13 relations) for any pair where
a unique answer is not encoded — correct but uninformative. The full
table lands in Phase 5; this function is a stable surface for callers
written against it now.

* **Return type:**
  [`set`](https://docs.python.org/3/builtins/stdtypes.html#set)[[`AllenRelation`](_autosummary/lacing.allen.html.md#lacing.allen.AllenRelation)]

### lacing.allen.intersects(a, b)

True iff `a` and `b` share any positive measure of time.

`meets`/`met_by` are NOT intersections (half-open boundaries share
zero measure). Two zero-length point intervals at the same instant DO
intersect (they’re equal).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### lacing.allen.relate(a, b)

Return the unique Allen relation between two well-formed intervals.

Exactly one of the 13 relations holds for any pair of well-formed
(`start <= end`) intervals.

Caveat: when both `a` and `b` are point intervals (zero-length),
`meets` and `met_by` would both technically apply alongside `equals`.
We resolve that by preferring `equals` for identical intervals; the
`meets`/`met_by` predicates exclude `a == b` accordingly.

* **Return type:**
  [`AllenRelation`](_autosummary/lacing.allen.html.md#lacing.allen.AllenRelation)


# _autosummary/lacing.artifact.html.md

# lacing.artifact

Content-addressed artifact references.

An [`Artifact`](_autosummary/lacing.artifact.html.md#lacing.artifact.Artifact) is a generated file (or remote URL) with provenance.
Anything that *produces* media — a fal.ai call, a `ffmpeg` compose, a
storyboard PDF render, an audio synthesis — returns an `Artifact`.
Anything that *references* an artifact temporally does so via
`MediaRef(asset_id=artifact.asset_id, interval=…)`.

This type lives in lacing (not in falaw or any single producer) because:

- It reuses [`lacing.Provenance`](_autosummary/lacing.html.md#lacing.Provenance) and [`lacing.MediaRef`](_autosummary/lacing.html.md#lacing.MediaRef). The
  `asset_id` *is* the content hash that `MediaRef` already documents.
- Multiple producers (falaw, an, nw, artful, mixing) need to express
  “I produced a file.” Putting `Artifact` in any one of them forces a
  wrong-direction dependency from the others.
- The provenance chain (`was_derived_from`, `was_generated_by`) is the
  same shape for an annotation and an artifact. Reusing it unifies lineage.

## Hashing

`asset_id` is the SHA-256 hex digest of the artifact’s bytes. SHA-256 is
in the stdlib, deterministic, and compatible with the
`MediaRef.asset_id` documentation (“BLAKE3 / SHA-256”). Producers may use
BLAKE3 if they prefer; the consumer treats `asset_id` opaquely.

### Examples

```pycon
>>> from pathlib import Path
>>> from lacing.artifact import Artifact, hash_file
>>> # Create one from a path:
>>> import tempfile
>>> with tempfile.NamedTemporaryFile("wb", suffix=".bin", delete=False) as f:
...     _ = f.write(b"hello world")
...     p = Path(f.name)
>>> a = Artifact.from_path(p, kind="text", was_generated_by="test:doctest",
...                        was_attributed_to="user:thor")
>>> a.kind
'text'
>>> a.bytes_size
11
>>> len(a.asset_id) == 64  # SHA-256 hex
True
>>> a.path == p
True
```

An artifact that was *acquired* rather than made carries a [`Rights`](_autosummary/lacing.artifact.html.md#lacing.artifact.Rights)
record (`None` means “we made this”, not “unknown”):

```pycon
>>> from lacing.artifact import Rights
>>> a = Artifact.from_bytes(b"png", kind="image", was_generated_by="fetch:openverse",
...     was_attributed_to="user:thor",
...     rights=Rights(provider="openverse", license="by-sa", author="Ada"))
>>> a.rights.license
'by-sa'
>>> Artifact.from_bytes(b"x", kind="text", was_generated_by="t",
...     was_attributed_to="u").rights is None
True
```

Round-trip through JSON:

```pycon
>>> import json
>>> data = a.model_dump_json()
>>> b = Artifact.model_validate_json(data)
>>> b.asset_id == a.asset_id
True
```

### Module Attributes

| [`ArtifactKind`](_autosummary/lacing.artifact.html.md#lacing.artifact.ArtifactKind)   | Coarse mime-class.   |
|-----------------------------------------------------------------|----------------------|

### Functions

| [`hash_bytes`](_autosummary/lacing.artifact.html.md#lacing.artifact.hash_bytes)(data)                  | Return the canonical `asset_id` (SHA-256 hex) for `data`.             |
|------------------------------------------------------------------------------------|-----------------------------------------------------------------------|
| [`hash_file`](_autosummary/lacing.artifact.html.md#lacing.artifact.hash_file)(path, \*[, chunk_size]) | Return the canonical `asset_id` (SHA-256 hex) for the file at `path`. |

### Classes

| [`Artifact`](_autosummary/lacing.artifact.html.md#lacing.artifact.Artifact)(\*\*data)   | A content-addressed generated file with provenance.              |
|-----------------------------------------------------------------------|------------------------------------------------------------------|
| [`Rights`](_autosummary/lacing.artifact.html.md#lacing.artifact.Rights)(\*\*data)     | Who owns the bytes of an *acquired* artifact, and on what terms. |

### *class* lacing.artifact.Artifact(\*\*data)

Bases: `BaseModel`

A content-addressed generated file with provenance.

`asset_id` is the SHA-256 hex digest of the artifact’s bytes. Two
artifacts with the same `asset_id` are byte-identical regardless of
where they live — so caches keyed on `asset_id` are safe across
machines and re-runs.

`provenance` reuses [`lacing.Provenance`](_autosummary/lacing.html.md#lacing.Provenance) so the lineage chain
(`was_derived_from`, `was_generated_by`) is the same for artifacts
and annotations. An annotation referencing an artifact does so via
`MediaRef(asset_id=artifact.asset_id, …)`.

#### *classmethod* from_bytes(data, , kind, was_generated_by, was_attributed_to, path=None, url=None, was_derived_from=(), activity='create', generated_at_time=None, duration_s=None, mime=None, cost_usd=None, producer_call_id=None, rights=None)

Create an Artifact from in-memory bytes.

* **Return type:**
  [`Artifact`](_autosummary/lacing.artifact.html.md#lacing.artifact.Artifact)

#### *classmethod* from_path(path, , kind, was_generated_by, was_attributed_to, was_derived_from=(), activity='create', generated_at_time=None, duration_s=None, mime=None, cost_usd=None, producer_call_id=None, rights=None)

Create an Artifact from a local file. Hashes the file’s bytes.

* **Return type:**
  [`Artifact`](_autosummary/lacing.artifact.html.md#lacing.artifact.Artifact)

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### to_media_ref(interval)

Return a [`lacing.MediaRef`](_autosummary/lacing.html.md#lacing.MediaRef) pointing at this artifact.

Use this to attach an annotation to a region of the artifact:
`MediaRef(asset_id=artifact.asset_id, interval=…)`.

* **Return type:**
  MediaRef

### lacing.artifact.ArtifactKind

Coarse mime-class. Producers should pick the closest match; consumers
should switch on this *before* falling back to `mime`/`path.suffix`.

alias of [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[‘image’, ‘video’, ‘audio’, ‘json’, ‘text’, ‘binary’]

### *class* lacing.artifact.Rights(\*\*data)

Bases: `BaseModel`

Who owns the bytes of an *acquired* artifact, and on what terms.

The fields a Creative-Commons-family licence needs to discharge its
attribution duty (title, author, source, licence: “TASL”), named
literal-for-literal after the two records the federation already keeps —
`illustration.ImageResult` and `an.ir.assets.AssetSource` — so both map
onto this one without a rename table (`provider`, `id`, `license`,
`license_url`, `attribution`, `source_page_url`, `author`,
`author_url`, `cacheable`).

Semantics worth the trap they avoid:

- **\`\`Artifact.rights is None\`\` means “we made this”**, not “unknown”. An
  acquired artifact whose terms nobody recorded is a `Rights` with only
  `provider` set.
- **\`\`license is None\`\` means UNKNOWN, never unencumbered.** Free of
  obligations is stated, e.g. `license="cc0-1.0"`.
- **\`\`cacheable is None\`\` means not stated**; `False` forbids keeping the
  bytes, `True` permits it.
- `license` is an SPDX id (`"CC-BY-4.0"`) or a provider’s own code
  (`"by-sa"`); lacing records it verbatim and never classifies it
  (classification lives with the consumer, e.g. `an`’s licence classes).
- The record describes **this artifact’s own bytes**. A render that embeds
  a third-party image carries no `Rights` of its own; its obligations
  travel through `provenance.was_derived_from` and are rolled up by the
  consumer. `rights is None` on a derivative is *not* a clearance.

`provider` is required, so an empty `Rights()` cannot exist: an
artifact either has no record (we made it) or says where it came from.
Text fields are `None` or non-blank; a blank string is refused.

Caveat: an artifact catalog is keyed by `asset_id` (the content hash), so
two records for the *same bytes* are one row and the last write wins. A
rights-less record written after an acquired one with identical bytes
replaces it, rights included. Writers that re-record bytes they already
hold must carry the existing `rights` forward.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### lacing.artifact.hash_bytes(data)

Return the canonical `asset_id` (SHA-256 hex) for `data`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> hash_bytes(b"hello world")[:8]
'b94d27b9'
```

### lacing.artifact.hash_file(path, , chunk_size=1048576)

Return the canonical `asset_id` (SHA-256 hex) for the file at `path`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/lacing.artifact_store.html.md

# lacing.artifact_store

Artifact store — a metadata catalog and a blob store behind one facade.

An [`Artifact`](_autosummary/lacing.artifact.html.md#lacing.artifact.Artifact) describes a generated file; this module
*persists* artifacts. It is the storage layer that `reelee` (and any other
producer) builds on so artifacts survive a process restart and are reachable
from another machine — not just from the browser tab that generated them.

## Design

The store keeps **three concerns separate** (see
`misc/docs/Artifact Store Architecture — design notes.md`):

- **Data organization** — artifacts are *physically flat*: a **catalog**
  (`id -> record`) holds metadata, a **blob store** (`content_hash ->
  bytes`) holds the heavy bytes. Neither key encodes ownership or grouping.
- **Infrastructure mapping** — [`ArtifactStore`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore) is a thin *facade* that
  composes two **injected** key-value stores. The backing stores are ordinary
  `MutableMapping` objects, so the backend swaps (in-memory -> filesystem ->
  object store) by dependency injection, with no change to callers.
- **Access calculus** — deliberately *absent here*. [`ArtifactStore`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore) is
  an unprivileged primitive that checks nothing; a caller that needs access
  control wraps it in a permission-enforcing facade of its own.

Identity is an **explicit string key**, not assumed to be the content hash:
artifacts often get a stable id *before* their bytes exist (and hence before
they can be hashed). The catalog is therefore generic over the record type and
keyed by whatever id the caller chooses; the content hash, when known, is just
a field on the record and the key of the blob store.

Consistency: there is no transaction spanning the two stores, so `save()`
writes the **blob first, then the catalog row**. Because blobs are
content-addressed the blob write is idempotent; a crash in between leaves at
worst an unreferenced (orphan) blob — never a catalog row pointing at missing
bytes.

### Examples

```pycon
>>> from lacing import Artifact
>>> store = ArtifactStore.in_memory()
>>> art = Artifact.from_bytes(
...     b"fake-png-bytes", kind="image",
...     was_generated_by="agent:flux", was_attributed_to="user:thor",
... )
>>> _ = store.save(art.asset_id, art)
>>> store[art.asset_id].kind
'image'
>>> art.asset_id in store
True
>>> len(store)
1
```

### Classes

| [`ArtifactStore`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore)(catalog[, blobs])   | Facade over an artifact `catalog` and an optional `blobs` store.   |
|------------------------------------------------------------------------------------|--------------------------------------------------------------------|

### *class* lacing.artifact_store.ArtifactStore(catalog, blobs=None)

Bases: [`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)

Facade over an artifact `catalog` and an optional `blobs` store.

The object *is* a `MutableMapping[str, record]` over the catalog —
`store[artifact_id]`, iteration, `len`, `get`, `clear` and the
rest of the mapping surface all act on artifact **metadata records**. The
heavier byte operations ([`put_blob()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.put_blob), [`get_blob()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.get_blob),
[`has_blob()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.has_blob)) are rich methods that are deliberately *not* squeezed
into the mapping protocol.

* **Parameters:**
  * **catalog** ([`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), `BaseModel`]) – Injected `id -> record` store. Records are pydantic models
    (`lacing.Artifact` by default, but any `BaseModel` works — the
    store does not inspect the record’s shape).
  * **blobs** ([`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Injected `content_hash -> bytes` store, or `None` for a
    catalog-only store (Stage-1 metadata persistence). Blob methods
    raise / no-op when it is `None`. This is the *raw* backend:
    on a filesystem backend its own `del` and iteration do no
    containment check, so go through [`delete_blob()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.delete_blob) and
    [`iter_blobs()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.iter_blobs) (and the read methods) rather than touching
    `store.blobs` directly (lacing#55).

Construct one with [`in_memory()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.in_memory) or [`from_directory()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.from_directory) rather than
wiring the backing stores by hand, unless you are injecting a custom
backend.

#### blob_location(content_hash)

Resolve the cheapest *servable* location for a blob — without
reading its bytes. The capability the HTTP layer probes to serve a
blob the most efficient way, generalizing [`blob_path()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.blob_path) so
filesystem / S3 / R2 backends all answer one probe:

- **object-store** backends (S3/R2) that expose a presigned-URL
  capability — a `url_for(content_hash)` callable — return a \*\*URL
  string\*\*, so the caller can 302-redirect and let the object store
  serve the bytes (and HTTP `Range`) directly, off the app process;
- **filesystem** backends return a local **Path** (see
  [`blob_path()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.blob_path)) so the caller hands the OS the file (Range free);
- everything else (plain `dict`, or a missing blob) returns
  `None` — the cue to fall back to [`iter_blob()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.iter_blob) streaming.

Callers treat `None` as “stream it”, not as an error.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### blob_path(content_hash)

Return the local filesystem path of the blob, or `None`.

The store’s blob backend is opaque (any `MutableMapping`), but some
backends — notably the filesystem-backed `dol.Files` produced by
[`from_directory()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.from_directory) — store each blob as one file under a known
root directory. This method exposes that path *when available*, so a
caller (e.g. a FastAPI route serving video) can hand the OS the file
descriptor and let it answer HTTP `Range` requests directly. It
returns `None` for:

- blob stores without a `rootdir` attribute (e.g. plain `dict`,
  object-store backends — callers should fall back to
  [`iter_blob()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.iter_blob));
- blobs that are not present.
- a `content_hash` that does not resolve to a path *inside*
  `rootdir` (path traversal, e.g. `"../../etc/passwd"` or an
  absolute path, or a same-named symlink pointing outside the
  store) — refused rather than served (lacing#50).

Callers must treat `None` as the cue to use the streaming read
path, not as an error. Deleting and listing blobs have contained
counterparts too ([`delete_blob()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.delete_blob), [`iter_blobs()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.iter_blobs)), and the
[`from_directory()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.from_directory) catalog confines artifact ids the same way
(lacing#55).

The containment check is *point-in-time*: the returned path is the
fully resolved, symlink-free location of a regular file that was
inside `rootdir` when checked. The caller opens it later, by name,
so if untrusted parties can write into `rootdir` itself, open it
with `O_NOFOLLOW` — or serve via [`iter_blob()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.iter_blob), which reads
through a descriptor and has no such window.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### count_refs(content_hash)

How many catalog records point at `content_hash` (a blob).

The reference count that a garbage collector needs: a blob is safe to
delete only when **no** catalog record still names its content hash.
Content-addressed blobs are deduplicated, so two artifacts can share
one blob — deleting the blob the moment one of them goes away would
orphan the other.

This is the *probe* half of a capability pair (mirroring
[`blob_location()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.blob_location) probing a blob store for `url_for`): a catalog
backend that can answer the count cheaply — the SQL catalog from
[`from_sql()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.from_sql), via an indexed `content_hash` column — exposes a
`refcount_by_content_hash` callable, and this method uses it so the
count is **one indexed query, not a full catalog scan**. Any other
catalog (dict, `Files`) falls back to scanning every record. The
facade is unchanged either way; only the speed differs.

* **Parameters:**
  **content_hash** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The blob’s content hash (`Artifact.asset_id`).
* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)
* **Returns:**
  The number of catalog records whose content hash equals
  `content_hash`.

#### delete_blob(content_hash)

Delete the blob `content_hash`; `KeyError` if there is none.

The contained counterpart of `del store.blobs[content_hash]`
(lacing#55). On a filesystem-backed store (one exposing `rootdir`)
a key is deleted only if [`has_blob()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.has_blob) would say it exists — a
regular file that resolves inside `rootdir` (see
`_ContainedDirStore`, the same gate the catalog of
[`from_directory()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.from_directory) uses). A key that escapes (`..`, an absolute path, a
NUL, a symlink pointing outside) raises `KeyError` exactly like a
missing blob, as does any key with a `..` segment, and nothing
outside the root is touched. The deletion itself is delegated to the
backend’s own `__delitem__` on the key as given (normalised), so the
backend’s delete policy (e.g. `dol.Files` moving files to the
trash) is unchanged, and deleting an in-root symlink removes the
link, never the blob it points at.

The check is point-in-time, like [`blob_path()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.blob_path): a party that can
write into `rootdir` and swap a *directory* component of a nested
key for a symlink between the check and the delete can still redirect
it. Flat content-hash keys, which is all this store writes, have no
such component.

Backends without a `rootdir` (`dict`, object stores) have no
filesystem to escape; the key passes through to their `del`.

* **Raises:**
  [**KeyError**](https://docs.python.org/3/builtins/exceptions.html#KeyError) – no such blob, the key escapes the root, or no blob store
      is configured.
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### *classmethod* from_aws(bucket_name, uri, \*, record_type=<class 'lacing.artifact.Artifact'>, collection_name='artifact_catalog', prefix=None, s3_kwargs=None, sql_kwargs=None)

The production pairing: **S3-compatible blobs + SQL catalog**.

A thin convenience over [`from_s3()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.from_s3) (blobs) and [`from_sql()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.from_sql)
(catalog) so the common cloud deployment is one call. The blob store is
an `S3Store` (AWS S3 / Cloudflare R2 / MinIO / Supabase); the catalog
is the durable, queryable SQL table — Postgres in production, SQLite for
a smoke test. Vendor specifics live only in the two kwargs dicts.

* **Parameters:**
  * **bucket_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Object-store bucket for the content-addressed blobs.
  * **uri** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – SQLAlchemy URI for the catalog (`postgresql://…` in prod).
  * **record_type** ([`type`](https://docs.python.org/3/builtins/functions.html#type)[`BaseModel`]) – Pydantic model the catalog (de)serializes.
  * **collection_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – SQL table name for the catalog.
  * **prefix** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Optional key prefix within the bucket.
  * **s3_kwargs** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Extra kwargs forwarded to [`from_s3()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.from_s3) (credentials,
    `endpoint_url` for R2/MinIO/Supabase, `region_name`, …).
  * **sql_kwargs** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Extra kwargs forwarded to [`from_sql()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.from_sql)
    (`content_hash_of`, SQLAlchemy engine kwargs, …).
* **Return type:**
  [`ArtifactStore`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore)

#### *classmethod* from_directory(root, \*, record_type=<class 'lacing.artifact.Artifact'>)

An ArtifactStore persisted under `root`.

Lays out two subdirectories: `catalog/` (one `<id>.json` file per
record) and `blobs/` (one file per content hash). Both are `dol`
filesystem stores, so the same facade works unchanged over any other
`dol` backend (object storage, etc.) when injected directly.

Artifact ids are confined to `catalog/` (lacing#55): an id whose
`<id>.json` would resolve outside it — `..` segments, an absolute
path, a NUL, or a symlink pointing out — is refused with `KeyError`
on get, save and delete, and is simply not `in` the store. Nested
ids (`"a/b"`) that stay inside keep working, and listing never
follows a symlinked directory out of `catalog/`. See
`_ContainedDirStore`.

* **Parameters:**
  * **root** ([`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Directory to hold the store. Created if missing.
  * **record_type** ([`type`](https://docs.python.org/3/builtins/functions.html#type)[`BaseModel`]) – The pydantic model the catalog deserializes JSON into.
    Defaults to [`Artifact`](_autosummary/lacing.artifact.html.md#lacing.artifact.Artifact); callers with
    their own record schema pass their model here.
* **Return type:**
  [`ArtifactStore`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore)

#### *classmethod* from_s3(bucket_name, , catalog=None, prefix=None, \*\*s3_kwargs)

An ArtifactStore with **blobs in an S3-compatible object store**
(AWS S3 / Cloudflare R2 / MinIO / Supabase) via `s3dol`.

Content-addressed blobs are a natural fit for object storage: flat,
immutable, dedup-friendly, forever-cacheable keys. [`blob_location()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.blob_location)
returns a presigned GET URL (via s3dol’s `url_for`) so a serving layer
can 302-redirect and let the store deliver the bytes (and HTTP `Range`)
directly, off the app process.

The blob and catalog backends are independent by design: the catalog
defaults to an in-memory dict here — swap in a durable catalog (e.g. a
Postgres-backed `MutableMapping`) for production.

* **Parameters:**
  * **bucket_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The object-store bucket.
  * **catalog** ([`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), `BaseModel`] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – The `id -> record` catalog `MutableMapping`. Defaults
    to `{}` (in-memory).
  * **prefix** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Optional key prefix within the bucket (e.g. `"blobs"`).
  * **\*\*s3_kwargs** – Forwarded to `s3dol.s3_store` — `endpoint_url`
    (set for R2 / MinIO / Supabase), `region_name`, `profile`,
    `credentials`, `preset`, `anon`, `on_missing_bucket`.
    The pre-v1 spellings (`aws_access_key_id` /
    `aws_secret_access_key` / `aws_session_token` /
    `profile_name` / `make_bucket`) are still accepted and
    translated, with a `DeprecationWarning`.
* **Return type:**
  [`ArtifactStore`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore)

Requires `s3dol>=1` (and `boto3`) — imported lazily so the
dependency is only needed when this constructor is used.

#### *classmethod* from_sql(uri, \*, blobs=None, record_type=<class 'lacing.artifact.Artifact'>, collection_name='artifact_catalog', content_hash_of=None, \*\*db_kwargs)

An ArtifactStore with a **SQL-backed catalog** (durable, queryable)
via `sqldol` — the SQL counterpart of [`from_s3()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.from_s3)’s object store.

The catalog row schema is deliberately minimal and **vendor-neutral**:
one `TEXT` column holds the record serialized exactly as
[`from_directory()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.from_directory) serializes it (`record.model_dump_json` out,
`record_type.model_validate_json` in), and one indexed `content_hash`
column carries the record’s blob hash so the GC reference count
([`count_refs()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.count_refs)) is a single indexed query rather than a full scan.

Because the connection is just a SQLAlchemy URI, the *same* code runs
on SQLite for tests and on Postgres in production — that is the whole
point of the facade. Pick the backend with the `uri` alone:

```default
ArtifactStore.from_sql("sqlite:///artifacts.db")          # local / tests
ArtifactStore.from_sql("postgresql://u:p@host:5432/db")   # production
```

The catalog and blob backends are independent: pass `blobs` to pair a
SQL catalog with any blob store (e.g. `from_s3`’s `S3Store`), or
leave it `None` for a catalog-only store (Stage-1 metadata
persistence; see [`from_aws()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.from_aws) for the common S3 + SQL pairing).

* **Parameters:**
  * **uri** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – SQLAlchemy connection URI (`sqlite:///…` or
    `postgresql://…`). The single knob that selects the vendor.
  * **blobs** ([`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Optional `content_hash -> bytes` blob store. `None` for a
    catalog-only store.
  * **record_type** ([`type`](https://docs.python.org/3/builtins/functions.html#type)[`BaseModel`]) – The pydantic model the catalog deserializes JSON into.
    Defaults to [`Artifact`](_autosummary/lacing.artifact.html.md#lacing.artifact.Artifact); callers with
    their own record schema pass their model here.
  * **collection_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The SQL table name for the catalog.
  * **content_hash_of** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[`BaseModel`], [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – How to read a record’s blob hash for the indexed
    `content_hash` column. Defaults to `asset_id` (the canonical
    Artifact field), then `content_hash`. Pass a callable for a
    record whose hash lives elsewhere; records without a hash store
    an empty string.
  * **\*\*db_kwargs** – Forwarded to `sqldol.SQLAlchemyStore` /
    SQLAlchemy’s `create_engine` (e.g. `connect_args`,
    `pool_size`).
* **Return type:**
  [`ArtifactStore`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore)

Requires `sqldol` (and `SQLAlchemy`) — imported lazily so the
dependency is only needed when this constructor is used.

#### get_blob(content_hash)

Return the bytes for `content_hash`, or `None` if absent.

On a filesystem-backed store (one exposing `rootdir`) the bytes are
read through `_open_within()`, never by re-opening the path by
name, so a key that would land outside `rootdir` — by `..`, an
absolute path, or a symlink, including one swapped in *after* the
containment check (lacing#50) — reads as absent.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### has_blob(content_hash)

Whether the blob store holds `content_hash`.

On a filesystem-backed store this is exactly “would [`get_blob()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.get_blob)
return bytes”: `False` for a key that resolves outside `rootdir`
or names anything but a regular file — see `_open_within()`.
Backends without a `rootdir` (`dict`, object stores) have no
filesystem to escape, so their keys pass through unfiltered.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

#### *classmethod* in_memory()

An ArtifactStore backed entirely by in-memory dicts.

For tests, scratch work, and as the trivial reference backend. Nothing
persists across processes.

* **Return type:**
  [`ArtifactStore`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore)

#### index()

Return the whole catalog as a plain dict (e.g. for UI hydration).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), `BaseModel`]

#### iter_blob(content_hash, , chunk_size=65536)

Yield the blob’s bytes in `chunk_size` chunks.

The streaming counterpart to [`get_blob()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.get_blob) — what an HTTP response
body iterates over when serving a large blob without holding it all
in process memory. The default implementation reads the whole blob
via [`get_blob()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.get_blob) and re-chunks it; a filesystem-backed store can
be swapped for a true streaming reader without changing this API.

* **Raises:**
  [**KeyError**](https://docs.python.org/3/builtins/exceptions.html#KeyError) – no blob exists for `content_hash`.
* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]

#### iter_blobs()

Yield the content hash (key) of every blob in the blob store.

The contained counterpart of `iter(store.blobs)` (lacing#55). On a
filesystem-backed store it yields exactly the keys [`has_blob()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.has_blob)
accepts — regular files inside `rootdir` — and never descends into
a symlinked directory (`dol.Files` does, so its listing can yield
files that live outside the root, or loop on a symlink cycle).
Hidden entries are skipped, as `dol.Files` skips them, which also
keeps the in-flight `.blob-*.part` spool files of
[`put_blob_stream()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.put_blob_stream) out of the listing.

Yields nothing when no blob store is configured; other backends are
iterated as they are.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

#### put_blob(data)

Store `data` content-addressed; return its content hash.

Idempotent: identical bytes always map to the same hash and overwrite
an identical blob. Delegates to [`put_blob_stream()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.put_blob_stream) so there is
exactly one write path — and therefore one atomicity story
(lacing#25).

* **Raises:**
  [**RuntimeError**](https://docs.python.org/3/builtins/exceptions.html#RuntimeError) – no blob store is configured.
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### put_blob_stream(chunks)

Stream `chunks` content-addressed; return their SHA-256 hash.

The streaming-friendly counterpart to [`put_blob()`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore.put_blob) — callers hand
in an iterable (e.g. `requests.Response.iter_content`) instead of
materializing the whole bytestring upfront. The hash is computed on
the fly.

When the blob store is filesystem-backed (exposes `rootdir`, as the
default directory store does), the bytes spool straight to a
same-directory tempfile and are renamed into place with
`os.replace` once the hash is known (lacing#25). Consequences a
caller may rely on:

- **peak memory is one chunk**, not 2× the payload — 100 MB videos
  flow through without a 200 MB spike;
- **a partial blob is never observable under its content address** —
  the name a reader could find only exists after the rename, which is
  atomic on POSIX and on Windows for same-directory renames — so
  `has_blob(h) == True` really does mean the full bytes are there;
- a failed or abandoned stream leaves nothing behind (the tempfile is
  unlinked).

Backends without a `rootdir` (plain `dict`, object-store
mappings) fall back to buffering the payload and assigning it whole —
their `__setitem__` is the atomicity story there.

* **Raises:**
  [**RuntimeError**](https://docs.python.org/3/builtins/exceptions.html#RuntimeError) – no blob store is configured.
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### save(artifact_id, record, , data=None)

Persist one artifact, optionally with its bytes.

Writes the blob (if `data` is given) **before** the catalog row, so a
crash never leaves the catalog pointing at missing bytes. Idempotent on
`artifact_id` — and, for the blob, on content — so retries are safe.

* **Parameters:**
  * **artifact_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The stable string id this artifact is filed under.
  * **record** (`BaseModel`) – The metadata record to store in the catalog.
  * **data** ([`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Optional raw bytes. When given, they are stored
    content-addressed and the content hash is returned; the caller
    is responsible for also recording that hash on `record`.
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)
* **Returns:**
  The blob’s content hash if `data` was written, else `None`.
* **Raises:**
  [**RuntimeError**](https://docs.python.org/3/builtins/exceptions.html#RuntimeError) – `data` was given but no blob store is configured.


# _autosummary/lacing.bodies.character_voice.html.md

# lacing.bodies.character_voice

Body schema for per-character voice profiles — the shared voice vocabulary.

URI: `annot://schema/character-voice/v1`

A character should sound the same in every line, with a chosen accent and
default emotional register — and “the way voice usually fails isn’t the
voice itself, it’s its delivery of the lines”. This body is where a
production pins that: one profile per character, read by whichever package
synthesizes a line.

Promoted here from reelee (lacing#10): the schema first shipped inside the
application layer, which meant nw and an — the packages the profile exists
to keep consistent ACROSS — could not read it without a dependency cycle.
lacing is the federation’s shared vocabulary layer; this module is the
issue’s own “Where it lives” section, finally honoured. The promotion is a
strict SUPERSET of the shipped shape: every field reelee wrote keeps its
name, type and default, so every stored body validates unchanged (no
migration), and reelee’s module becomes a re-export of this one.

What the promotion adds (the three groups the shipped subset dropped):

- `provider_voice_ids` — the per-provider map (“elevenlabs” → one id,
  “minimax” → another), so a profile survives a provider switch instead of
  being one provider’s id wearing a generic field name. `voice_id` stays
  as the primary/default id (and what every stored body already has).
- `default_emotion` — the register a character speaks in unless a line
  overrides it.
- `reference_sample_artifact_id` — a content-addressed clone sample
  (64-hex lacing `AssetId`; representable since lacing#14), for
  voice-cloning providers.

The ElevenLabs-flavoured knobs (`stability` / `similarity_boost` /
`style` / `speed` / `delivery`) stay — they are what the shipped
bodies carry and what braidio’s synthesis path reads — documented as
provider-generic 0-1 intents that a provider adapter maps or ignores.

### Classes

| [`CharacterVoiceBodyV1`](_autosummary/lacing.bodies.character_voice.html.md#lacing.bodies.character_voice.CharacterVoiceBodyV1)(\*\*data)   | Body of a character-voice annotation — one character's voice profile.   |
|-----------------------------------------------------------------------------------|-------------------------------------------------------------------------|

### *class* lacing.bodies.character_voice.CharacterVoiceBodyV1(\*\*data)

Bases: `SchemaModel`

Body of a character-voice annotation — one character’s voice profile.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].


# _autosummary/lacing.bodies.continuity_violation.html.md

# lacing.bodies.continuity_violation

Body schema for continuity violations — generic, cross-media.

URI: `annot://schema/continuity-violation/v1`

A *continuity violation* is a typed claim about a relationship between
two or more annotations: “panels 5 and 6 cross the 180° axis”, “the
character drift between panel 3 and panel 4 exceeds the embed-distance
threshold”, “object detection finds a prop in panel 7 absent from
panel 8.”

The schema is intentionally generic. Specific rule families live in
their producing packages — reelee defines axis_180, character_drift,
prop_hallucination, etc. via dedicated Transforms — but the *envelope*
that carries the result is generic enough to live in lacing.

Violations attach as `review/v1` annotations whose `review_kind ==
"continuity"` (see [`lacing.bodies.review`](_autosummary/lacing.bodies.review.html.md#module-lacing.bodies.review)).

Reference: `reelee/docs/Narrative to Storyboard.md` §6.5, §8.1.

### Module Attributes

| [`SuggestedAction`](_autosummary/lacing.bodies.continuity_violation.html.md#lacing.bodies.continuity_violation.SuggestedAction)   | Coarse action the edit-loop UI offers in response.   |
|--------------------------------------------------------------------|------------------------------------------------------|

### Classes

| [`ContinuityViolationBodyV1`](_autosummary/lacing.bodies.continuity_violation.html.md#lacing.bodies.continuity_violation.ContinuityViolationBodyV1)(\*\*data)   | Body of a continuity-violation annotation.   |
|----------------------------------------------------------------------------------------|----------------------------------------------|

### *class* lacing.bodies.continuity_violation.ContinuityViolationBodyV1(\*\*data)

Bases: `SchemaModel`

Body of a continuity-violation annotation.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### lacing.bodies.continuity_violation.SuggestedAction

Coarse action the edit-loop UI offers in response. Producing
Transforms pick one; the FE wires each to a button + acture command.

alias of [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[‘regen_panel’, ‘regen_with_ref’, ‘manual_edit’, ‘accept’]


# _autosummary/lacing.bodies.html.md

# lacing.bodies

Built-in body schemas.

Each module in this package defines one body schema (Pydantic v2 model)
and registers it under an `annot://schema/<name>/v<major>` URI.

These are the seed schemas users can reach for; they’re not authoritative
beyond demonstrating the pattern. Custom packages should register their
own under their own names.

Importing this package registers every built-in body schema.

### Modules

| [`character_voice`](_autosummary/lacing.bodies.character_voice.html.md#module-lacing.bodies.character_voice)           | Body schema for per-character voice profiles — the shared voice vocabulary.     |
|-----------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| [`continuity_violation`](_autosummary/lacing.bodies.continuity_violation.html.md#module-lacing.bodies.continuity_violation) | Body schema for continuity violations — generic, cross-media.                   |
| [`named_entity`](_autosummary/lacing.bodies.named_entity.html.md#module-lacing.bodies.named_entity)                 | Body schemas for named-entity (NER) annotations.                                |
| [`reference_lock`](_autosummary/lacing.bodies.reference_lock.html.md#module-lacing.bodies.reference_lock)             | Body schema for a *locked reference* — a first-class “this is canonical”.       |
| [`review`](_autosummary/lacing.bodies.review.html.md#module-lacing.bodies.review)                             | Body schema for review notes — the edit-loop's communication channel.           |
| [`review_candidate`](_autosummary/lacing.bodies.review_candidate.html.md#module-lacing.bodies.review_candidate)         | Body schema for review *candidates* — "this needs a human's eyes".              |
| [`word`](_autosummary/lacing.bodies.word.html.md#module-lacing.bodies.word)                                 | Body schema for word-level annotations (e.g., transcription, forced alignment). |


# _autosummary/lacing.bodies.named_entity.html.md

# lacing.bodies.named_entity

Body schemas for named-entity (NER) annotations.

URIs:

```default
annot://schema/named-entity/v1   -- type + text
annot://schema/named-entity/v2   -- entity_type + text + optional confidence (additive)
```

A v1 -> v2 migration is registered to demonstrate the pattern.

### Classes

| [`NamedEntityBodyV1`](_autosummary/lacing.bodies.named_entity.html.md#lacing.bodies.named_entity.NamedEntityBodyV1)(\*\*data)   | Original NER body.                                                 |
|--------------------------------------------------------------------------------|--------------------------------------------------------------------|
| [`NamedEntityBodyV2`](_autosummary/lacing.bodies.named_entity.html.md#lacing.bodies.named_entity.NamedEntityBodyV2)(\*\*data)   | v2 renames `type` -> `entity_type` and adds optional `confidence`. |

### *class* lacing.bodies.named_entity.NamedEntityBodyV1(\*\*data)

Bases: `SchemaModel`

Original NER body. `type` is the entity tag (PER, ORG, LOC, …).

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* lacing.bodies.named_entity.NamedEntityBodyV2(\*\*data)

Bases: `SchemaModel`

v2 renames `type` -> `entity_type` and adds optional `confidence`.

The rename makes v2 incompatible with v1, so we register a migration.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].


# _autosummary/lacing.bodies.reference_lock.html.md

# lacing.bodies.reference_lock

Body schema for a *locked reference* — a first-class “this is canonical”.

URI: `annot://schema/reference-lock/v1`

A **reference lock** records the decision that one or more reference
artifacts *are* the canonical anchor for some subject — a character’s
face, a location’s architecture, a style, a prop. Once a subject is
locked, a *supervisor* can check every later generation against the
locked anchor and flag drift (the consumer in reelee runs the
identity/likeness comparison and emits `continuity-violation/v1`
annotations; this schema only carries the *decision*, not the result).

The shape is deliberately general so it fits several real workflows
without a schema bump:

- **One image or a set.** `locked_artifact_ids` holds 1..N artifacts.
  Lock a single canonical headshot, or a set (front / three-quarter /
  full-body / expression sheet). `primary_artifact_id` names the one
  to show by default.
- **Project- or scene-scoped.** `scope` is `"project"` by default
  (the character is locked for the whole piece) or `"scene"` (a
  re-lock for a flashback / costume change), in which case
  `scope_ref` names the scene/segment it applies to. Re-locks chain
  via `supersedes` (the prior lock’s annotation id).
- **Per-aspect checklist with advisory-by-default gating.**
  `checklist` is the set of aspects the supervisor compares
  (`"face"`, `"architecture"`, `"props"`, `"lighting"`,
  `"costume"`, `"palette"`, …). `hard_gates` is the subset that
  *blocks* on mismatch; it defaults to empty, so every aspect is
  **advisory** (flag, never auto-reject) — honouring the field
  observation that over-strict likeness filters waste the user’s time
  rejecting perfectly good images.

The lock is itself an annotation; *who* locked it and *when* live in
the annotation’s PROV-O provenance, not in this body.

Reference: `reelee/docs/Narrative to Storyboard.md` (reference
consistency); reelee-web epic #151, lacing #9.

### Module Attributes

| [`SubjectKind`](_autosummary/lacing.bodies.reference_lock.html.md#lacing.bodies.reference_lock.SubjectKind)   | What the lock anchors.                                |
|----------------------------------------------------------------|-------------------------------------------------------|
| [`Scope`](_autosummary/lacing.bodies.reference_lock.html.md#lacing.bodies.reference_lock.Scope)         | `project` — locked for the whole piece (the default). |

### Classes

| [`ReferenceLockBodyV1`](_autosummary/lacing.bodies.reference_lock.html.md#lacing.bodies.reference_lock.ReferenceLockBodyV1)(\*\*data)   | Body of a reference-lock annotation.   |
|----------------------------------------------------------------------------------|----------------------------------------|

### *class* lacing.bodies.reference_lock.ReferenceLockBodyV1(\*\*data)

Bases: `SchemaModel`

Body of a reference-lock annotation.

Records that `locked_artifact_ids` are the canonical anchor for
`subject_id` (a `subject_kind`), the aspects a supervisor should
compare (`checklist`), which of those block vs merely warn
(`hard_gates`), and the advisory identity threshold
(`min_similarity`).

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### *property* primary *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The artifact id to display — `primary_artifact_id` or the first locked one.

### lacing.bodies.reference_lock.Scope

`project` — locked for the whole piece (the default). `scene` —
re-locked for one scene/segment (flashback, costume change); requires
`scope_ref`.

alias of [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[‘project’, ‘scene’]

### lacing.bodies.reference_lock.SubjectKind

What the lock anchors. `character` (a face/identity), `location`
(architecture / set), `style` (look anchor), `prop` (a recurring
object), or `other` for anything not yet enumerated.

alias of [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[‘character’, ‘location’, ‘style’, ‘prop’, ‘other’]


# _autosummary/lacing.bodies.review.html.md

# lacing.bodies.review

Body schema for review notes — the edit-loop’s communication channel.

URI: `annot://schema/review/v1`

A *review* annotation is a structured note attached to one or more
artifacts (a panel, a beat, a segmentation alternate, a render result),
optionally pointing at a typed payload like a
`ContinuityViolationBodyV1`. Reviews are produced by:

- automated checkers (continuity, grammar, timing, segmentation rules)
- the edit-loop (“this caption needs a revision”)
- the orchestrator (“the user declined the cost gate at panel 12”)
- the user (“approve” / “needs revision” notes)

The body is intentionally minimal. The kind (`review_kind`) routes
the FE renderer; the payload is opaque to lacing.

An `approval` review carries a `decision` — this is the record
`lacing.server.operations.accept_ai_suggestion()` writes when a human
accepts or rejects an AI suggestion, so that the human’s edit is
attributed to the human without overwriting the agent’s provenance on the
annotation being judged (lacing#18).

#### NOTE
`decision` is **additive and optional**, so stored bodies validate
unchanged and lacing owes no migration. Downstream mirrors of this
schema are a different matter: reelee-web’s generated Zod type is
`.strict()` and its JSON Schema is `additionalProperties: false`,
so an additive field here is a *breaking* change there until the
mirror is regenerated — tracked as thorwhalen/reelee-web#234.

### References

- `reelee/docs/Narrative to Storyboard.md` §6.5–6.6.
- `reelee/docs/reelee 03 -- Human-AI Collaboration UX Patterns…` §10
  (annotation overlays).

### Module Attributes

| [`ReviewKind`](_autosummary/lacing.bodies.review.html.md#lacing.bodies.review.ReviewKind)     | What flavor of review this is.                                      |
|-----------------------------------------------------------------|---------------------------------------------------------------------|
| [`ReviewStatus`](_autosummary/lacing.bodies.review.html.md#lacing.bodies.review.ReviewStatus)   | Open is the initial state; the others are user / agent resolutions. |
| [`Author`](_autosummary/lacing.bodies.review.html.md#lacing.bodies.review.Author)         | Coarse author classification.                                       |
| [`ReviewDecision`](_autosummary/lacing.bodies.review.html.md#lacing.bodies.review.ReviewDecision) | The verdict an `approval` review carries.                           |

### Classes

| [`ReviewBodyV1`](_autosummary/lacing.bodies.review.html.md#lacing.bodies.review.ReviewBodyV1)(\*\*data)   | Body of a review annotation.   |
|---------------------------------------------------------------------------|--------------------------------|

### lacing.bodies.review.Author

Coarse author classification. The annotation envelope’s
`was_attributed_to` carries the specific identity; this field is the
quick chip for the FE.

alias of [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[‘human’, ‘agent’, ‘system’]

### *class* lacing.bodies.review.ReviewBodyV1(\*\*data)

Bases: `SchemaModel`

Body of a review annotation.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### lacing.bodies.review.ReviewDecision

The verdict an `approval` review carries. Only `review_kind =
"approval"` reviews have one — a continuity or grammar note is an
observation, not a ruling, so `decision` stays `None` there.

Why this is a field rather than something to read off the reviewed
annotation’s `confidence`: the verdict is a fact about \*the review
event\*, and `confidence` is a mutable field on a different record that
the next write can move. Recovering “was this accepted?” by dereferencing
a live value is exactly the coupling that made lacing#18 possible.

alias of [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[‘accepted’, ‘rejected’]

### lacing.bodies.review.ReviewKind

What flavor of review this is. Drives the FE’s renderer choice and
the producing-Transform’s routing logic.

alias of [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[‘continuity’, ‘segmentation’, ‘prompt’, ‘grammar’, ‘timing’, ‘iconic_moment’, ‘manual’, ‘approval’]

### lacing.bodies.review.ReviewStatus

Open is the initial state; the others are user / agent resolutions.

alias of [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[‘open’, ‘addressed’, ‘wont_fix’, ‘deferred’]


# _autosummary/lacing.bodies.review_candidate.html.md

# lacing.bodies.review_candidate

Body schema for review *candidates* — “this needs a human’s eyes”.

URI: `annot://schema/review-candidate/v1`

A review-candidate is what an automated processor emits when it flags an
annotation as worth a human look — today,
[`lacing.processors.low_confidence_review()`](_autosummary/lacing.processors.html.md#lacing.processors.low_confidence_review) flagging low-confidence
rows. It is deliberately NOT a [`ReviewBodyV1`](_autosummary/lacing.bodies.review.html.md#lacing.bodies.review.ReviewBodyV1):

- a review is a structured **note or verdict** (a continuity observation,
  an approval carrying a `decision`) written *about* work;
- a candidate is a **pointer into a queue** — “look at this one, and here
  is why it was flagged” — with no message, no status lifecycle, and no
  verdict. Reviewing a candidate *produces* a review; the candidate is the
  input, not an early draft of the output.

The two lived under one URI for a while, and the processor’s rows failed
validation against the model registered for that URI in five ways at once
(lacing#37). One URI per body shape is the contract `body_schema_uri`
exists to keep; giving the candidate its own name is the whole fix — no
stored `review/v1` body changes shape, so no migration.

The tier split mirrors the body split, stated here because it is easy to
read as an accident: candidates land on `"for-review"` (the processor’s
default) while human/agent reviews land on `"review"`
(`operations.DFLT_REVIEW_TIER`). Candidates queue; reviews record.

### Classes

| [`ReviewCandidateBodyV1`](_autosummary/lacing.bodies.review_candidate.html.md#lacing.bodies.review_candidate.ReviewCandidateBodyV1)(\*\*data)   | Body of a review-candidate annotation.   |
|------------------------------------------------------------------------------------|------------------------------------------|

### *class* lacing.bodies.review_candidate.ReviewCandidateBodyV1(\*\*data)

Bases: `SchemaModel`

Body of a review-candidate annotation.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].


# _autosummary/lacing.bodies.word.html.md

# lacing.bodies.word

Body schema for word-level annotations (e.g., transcription, forced alignment).

URI: `annot://schema/word/v1`

### Classes

| [`WordBodyV1`](_autosummary/lacing.bodies.word.html.md#lacing.bodies.word.WordBodyV1)(\*\*data)   | A single word annotation.   |
|-------------------------------------------------------------------------|-----------------------------|

### *class* lacing.bodies.word.WordBodyV1(\*\*data)

Bases: `SchemaModel`

A single word annotation.

The `text` is the surface form as it appears in the source media.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].


# _autosummary/lacing.cli.html.md

# lacing.cli

Command-line interface for lacing.

Built with `cw` (argparse + signature-driven argument inference).
The entry point is `lacing` — see `project.scripts` in pyproject.toml.

Subcommands:

> lacing convert <src> <dst> [–src-format] [–dst-format] [–rate]
> : Convert between any two registered formats. Inferred from
>   extension; override with `--src-format` / `--dst-format`.

> lacing query <path> [–tier] [–start] [–end] [–relation]
> : Print annotations matching a tier and/or interval query.
>   Output is JSON-lines on stdout.

> lacing validate <path> [–rate]
> : Load + re-dump round-trip; print a summary. Useful for sanity
>   checking files before adopting them.

> lacing migrate <path> [–to-version N]
> : Upgrade a `.annot` file to the current store schema, in place,
>   via the registered store-migration ladder. Explicit by design:
>   opening a stale file never rewrites it.

> lacing list-formats
> : Print every registered format adapter.

The CLI imports each Phase 0/1 adapter at startup so they self-register.
Add new adapters by editing `_ENABLED_ADAPTERS` in this module.

## Type annotations are load-bearing here

This module has `from __future__ import annotations`, so every annotation
below is a *string* at runtime. `argh`, which this CLI used to be built with,
reads `__annotations__` raw and is therefore blind to PEP 563: under it,
`--start`, `--end` and `--to-version` all arrived as `str` no matter what
their annotations said, and the only defence was hand-written coercion in the
command body (which `migrate` had and `query` did not — so
`lacing query f.vtt --start abc --end 2.0` used to get all the way into
`RationalTime` before failing).

`_CONVENTION` turns that off. `resolve_hints=True` makes `cw` resolve
annotations with [`typing.get_type_hints()`](https://docs.python.org/3/library/typing.html#typing.get_type_hints) instead of reading them raw, so
`start: float | None` becomes `type=float` at argparse’s `type=` site —
where a bad value is a clean `usage:` + exit 2 rather than a traceback from
somewhere in the call stack. Which means: \*\*annotate the parameters, and do not
coerce in the body.\*\*

### Functions

| [`convert`](_autosummary/lacing.cli.html.md#lacing.cli.convert)(src, dst, \*[, src_format, ...])           | Convert between annotation file formats.                                                                                                  |
|-----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|
| [`list_formats`](_autosummary/lacing.cli.html.md#lacing.cli.list_formats)()                                     | Print every registered format adapter.                                                                                                    |
| [`main`](_autosummary/lacing.cli.html.md#lacing.cli.main)([argv])                                       | Entry point for the `lacing` console script.                                                                                              |
| [`migrate`](_autosummary/lacing.cli.html.md#lacing.cli.migrate)(path, \*[, to_version])                    | Upgrade PATH (a `.annot` file) to the current store schema, in place.                                                                     |
| [`mk_parser`](_autosummary/lacing.cli.html.md#lacing.cli.mk_parser)()                                        | Build the `lacing` parser — a plain [`argparse.ArgumentParser`](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser). |
| [`query`](_autosummary/lacing.cli.html.md#lacing.cli.query)(path, \*[, tier, start, end, relation, ...]) | Print annotations from PATH matching a tier and/or time-interval query.                                                                   |
| [`validate`](_autosummary/lacing.cli.html.md#lacing.cli.validate)(path, \*[, src_format, rate])             | Round-trip PATH through load + dump and report a summary.                                                                                 |

### lacing.cli.convert(src, dst, , src_format=None, dst_format=None, rate=24000)

Convert between annotation file formats.

SRC is a path to a recognized file (or `-` for stdin bytes — not yet).
DST is the output path. Formats are inferred from extensions; override
with –src-format / –dst-format.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### lacing.cli.list_formats()

Print every registered format adapter.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### lacing.cli.main(argv=None)

Entry point for the `lacing` console script.

`cw.run` *returns* the exit code where argh’s `dispatch` exited by
itself, so the `SystemExit` here is what makes `lacing no-such-command`
exit 2 rather than 0.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### lacing.cli.migrate(path, , to_version=None)

Upgrade PATH (a `.annot` file) to the current store schema, in place.

Migration is explicit — opening a stale file never rewrites it — so this
verb is the ladder’s front door. Already-current files are a no-op.
`--to-version` upgrades part-way (mainly useful in tests).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### lacing.cli.mk_parser()

Build the `lacing` parser — a plain [`argparse.ArgumentParser`](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser).

* **Return type:**
  [`ArgumentParser`](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser)

### lacing.cli.query(path, , tier=None, start=None, end=None, relation='intersects', src_format=None, rate=24000, limit=100)

Print annotations from PATH matching a tier and/or time-interval query.

Output is JSON-lines on stdout. `--start` and `--end` are seconds.
`--relation` is one of: intersects, during, contains, overlaps, meets,
starts, finishes, equals.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### lacing.cli.validate(path, , src_format=None, rate=24000)

Round-trip PATH through load + dump and report a summary.

Useful for confirming an external file is well-formed and that the
adapter understands it.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/lacing.digest.html.md

# lacing.digest

Content digests over an annotation’s *value* — the freshness primitive.

lacing has **three** digests and they answer **three different questions**.
Collapsing any two of them is a silent correctness bug, so the boundary is
spelled out here and cross-referenced from the other two:

- [`lacing.hash_bytes()`](_autosummary/lacing.html.md#lacing.hash_bytes) / [`lacing.hash_file()`](_autosummary/lacing.html.md#lacing.hash_file) — SHA-256 over an
  **artifact’s bytes**. Answers  *“are these two files the same file?”*. This
  is the `Artifact.asset_id` contract ([`lacing.artifact`](_autosummary/lacing.artifact.html.md#module-lacing.artifact)).
- `lacing.server.etag.annotation_etag()` — BLAKE2b-128 over the \*\*whole
  annotation\*\*, `id` and `provenance` included. Answers  *“has this record
  been touched since I read it?”*. This is the `If-Match` / HTTP 412
  optimistic-concurrency primitive, and it is *deliberately* unstable across
  regenerations.
- [`annotation_value_digest()`](_autosummary/lacing.digest.html.md#lacing.digest.annotation_value_digest) (this module) — SHA-256 over an annotation’s
  **value**, `id` and `provenance` excluded. Answers  *“did the answer
  actually change?”*. This is the freshness / early-cutoff primitive.

A regeneration that produces byte-identical content mints a fresh `uuid4`
`id` and a fresh `provenance.generated_at_time`, so `annotation_etag`
changes while `annotation_value_digest` does not. That difference is the
entire point: it lets a downstream freshness check key on \*upstream output
values\* rather than on *upstream keys*, and stop propagating invalidation
when nothing actually changed.

## Why this lives in core and not under `lacing/server/`

`annotation_etag` sits at `lacing/server/etag.py`. Its own imports are
cheap, but importing any submodule of `lacing.server` executes
`lacing/server/__init__.py`, which imports the FastAPI app. The value
digest is consumed by the *execution* tier (`nw`, `falaw`), which must
not drag a web framework in. This module therefore has \*\*no runtime imports
beyond the standard library\*\* — the [`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation) import
is type-checking only.

## The inclusion boundary — and why each call was made

Getting this boundary wrong is a silent wrong-cache-**hit**, not a miss, so
each field is justified rather than assumed.

**Included** ([`VALUE_FIELDS`](_autosummary/lacing.digest.html.md#lacing.digest.VALUE_FIELDS)):

- `body` — it *is* the annotation’s value. Trivially included.
- `body_schema_uri` — the same `dict` means different things under
  different schemas, and a schema-version bump is a semantic change even when
  the payload bytes are unchanged. Excluding it would let a `.../v1` body
  satisfy a `.../v2` consumer’s cache lookup.
- `tier` — the tier is part of what the annotation asserts (which layer of
  the analysis this claim belongs to), and lacing’s tier stereotypes impose
  structural constraints. Two identical bodies on different tiers are
  different claims.
- `reference` — what the annotation is *about*: the asset and the interval.
  Re-timing an annotation changes its value. This is the one genuinely
  contested call (see below); it is included here and excluded from
  [`annotation_body_digest()`](_autosummary/lacing.digest.html.md#lacing.digest.annotation_body_digest), so the consumer chooses rather than guesses.
- `confidence` — a soft label whose confidence moved is a changed assertion.
  Downstream thresholding consumes it directly.

**Excluded**:

- `id` — a fresh `uuid4` on every regeneration. Including it makes the
  digest change unconditionally, which is exactly the `annotation_etag`
  behaviour this function exists to avoid.
- `provenance` — `generated_at_time` changes on every run, and
  `was_generated_by` / `was_derived_from` describe *how* the value was
  reached, not *what* it is. Two byte-identical answers produced by different
  activities are the same answer. (If a consumer needs to invalidate on a
  changed producer, that belongs in its **cache key** — which is keyed on
  inputs — not in the **value digest**, which addresses the output.)

Rule of thumb, stated once: \*key the cache on inputs; address the value by
content; record both in the trace.\* A system with only the first cannot cut
off early.

## `reference`: two functions, not a boolean

Whether `reference` belongs in a value digest is genuinely
consumer-dependent. A [`MediaRef`](_autosummary/lacing.model.html.md#lacing.model.MediaRef) carries an
`interval`, so re-timing busts every downstream digest — **correct** when
downstream consumes the timing (an animatic, a cut list), **wasteful** when
it consumes only the body (a caption translation). Rather than a boolean flag
or a wrong guess that needs migrating later, this module ships both:

- [`annotation_value_digest()`](_autosummary/lacing.digest.html.md#lacing.digest.annotation_value_digest) — the full value, `reference` included.
  **The default.** Use it unless you can state why the rest is irrelevant.
- [`annotation_body_digest()`](_autosummary/lacing.digest.html.md#lacing.digest.annotation_body_digest) — `{body, body_schema_uri}` only.

\*\*Be precise about what the narrow one drops, because “re-timing” undersells
it.\*\* `annotation_body_digest` drops the *entire* `reference` — the asset
identity as well as the interval — plus `tier` and `confidence`. Two
annotations carrying the same body over **different assets** digest alike
under it. Reach for it only when the consumer depends on nothing but what the
annotation *says*.

They are domain-separated (see [`VALUE_DIGEST_SCHEME`](_autosummary/lacing.digest.html.md#lacing.digest.VALUE_DIGEST_SCHEME)), so the two can
never collide even on an annotation whose payloads coincide.

## Stability guarantees, and their limits

The digest is stable **across processes**, **across dict insertion order**
(`sort_keys=True` canonicalises recursively) and \*\*across a store
round-trip\*\* (memory → `.annot` → memory).

## The safety claim, stated precisely

\*\*For a body that honours the `body` contract — i.e. contains only JSON
types, which is what validating against JSON Schema means — this digest never
returns a wrong cache *hit*.\*\* Two annotations with different values never
digest alike. It can return a spurious *miss*; that only costs a recompute.

That claim is bounded by the contract, and it is worth knowing exactly where
the boundary is, because an unbounded version of it would be false:

1. \*\*Non-`str` mapping keys raise\*\* [`NonStringBodyKeyError`](_autosummary/lacing.digest.html.md#lacing.digest.NonStringBodyKeyError).
   `model_dump(mode="json")` coerces keys to strings, so a body like
   `{1: "a", "1": "b"}` collapses to `{"1": "b"}` — an entire entry is
   annihilated, and two bodies differing only in the annihilated entry would
   digest **alike**. That is a wrong hit, so it is refused rather than
   documented. Such a body is already broken data: it does not survive a
   round-trip through any lacing store either (JSON object keys are strings).
2. **Python container types that JSON cannot distinguish digest alike** —
   `(1, 2)` and `[1, 2]` both serialise to `[1, 2]`. This is *not* a
   wrong hit within the contract: as JSON they are the same value, and a
   contract-honouring body cannot contain a tuple. A `set` additionally
   serialises in an order not stable across processes (a spurious miss), and
   a type pydantic cannot serialise at all raises
   `PydanticSerializationError` rather than digesting a `repr` that embeds
   a memory address.
3. **Equal-but-differently-serialised rationals digest differently.**
   [`RationalTime`](_autosummary/lacing.time.html.md#lacing.time.RationalTime) serialises as `{"v": …, "r": …}`, so
   > `RationalTime(1, 24)` and `RationalTime(2, 48)` compare **equal** but
   > digest differently — a rate change moves the digest even when the instant
   > is unchanged. This is a spurious *miss*. (`annotation_etag` has the same
   > property.)

Changing [`VALUE_FIELDS`](_autosummary/lacing.digest.html.md#lacing.digest.VALUE_FIELDS) or the canonicalisation is a \*\*breaking cache
invalidation event\*\* for every consumer: every digest changes at once. Bump
the scheme string when you do it, so the change is legible in a trace.

### Module Attributes

| [`VALUE_FIELDS`](_autosummary/lacing.digest.html.md#lacing.digest.VALUE_FIELDS)        | Annotation fields that constitute its *value*.                                                               |
|----------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------|
| [`BODY_FIELDS`](_autosummary/lacing.digest.html.md#lacing.digest.BODY_FIELDS)         | The narrower payload for [`annotation_body_digest()`](_autosummary/lacing.digest.html.md#lacing.digest.annotation_body_digest).          |
| [`VALUE_DIGEST_SCHEME`](_autosummary/lacing.digest.html.md#lacing.digest.VALUE_DIGEST_SCHEME) | Domain-separation tag mixed into [`annotation_value_digest()`](_autosummary/lacing.digest.html.md#lacing.digest.annotation_value_digest). |
| [`BODY_DIGEST_SCHEME`](_autosummary/lacing.digest.html.md#lacing.digest.BODY_DIGEST_SCHEME)  | Domain-separation tag mixed into [`annotation_body_digest()`](_autosummary/lacing.digest.html.md#lacing.digest.annotation_body_digest).  |

### Functions

| [`annotation_value_digest`](_autosummary/lacing.digest.html.md#lacing.digest.annotation_value_digest)(annotation)   | Return the SHA-256 hex digest of `annotation`'s value.           |
|----------------------------------------------------------------------------------------|------------------------------------------------------------------|
| [`annotation_body_digest`](_autosummary/lacing.digest.html.md#lacing.digest.annotation_body_digest)(annotation)    | Return the SHA-256 hex digest of `{body, body_schema_uri}` only. |

### Exceptions

| [`NonStringBodyKeyError`](_autosummary/lacing.digest.html.md#lacing.digest.NonStringBodyKeyError)   | An annotation `body` contains a mapping key that is not a `str`.   |
|--------------------------------------------------------------------------|--------------------------------------------------------------------|

### lacing.digest.BODY_DIGEST_SCHEME *= 'lacing/annotation-body-digest/v1'*

Domain-separation tag mixed into [`annotation_body_digest()`](_autosummary/lacing.digest.html.md#lacing.digest.annotation_body_digest).

### lacing.digest.BODY_FIELDS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('body', 'body_schema_uri')*

The narrower payload for [`annotation_body_digest()`](_autosummary/lacing.digest.html.md#lacing.digest.annotation_body_digest).

Note what this drops, which is more than timing: the **entire** `reference`
(*which asset* / *which node* / *which annotation*, not just *when*), plus
`tier` and `confidence`. Two annotations over different assets digest
alike here.

### *exception* lacing.digest.NonStringBodyKeyError

Bases: [`TypeError`](https://docs.python.org/3/builtins/exceptions.html#TypeError)

An annotation `body` contains a mapping key that is not a `str`.

JSON object keys are strings, so `model_dump(mode="json")` coerces
non-string keys — and two distinct keys can coerce to the *same* string,
silently annihilating an entry. `{1: "a", "1": "b"}` dumps to
`{"1": "b"}`; a body differing only in the lost entry would digest
identically, which is a wrong cache **hit**.

Since lacing#24 the *envelope* refuses such a body at validation — the
producer-side fix. This error lives here rather than in `lacing.model`
because this module is deliberately import-light (stdlib only, pinned by
test) and the model can import from it, not vice versa.

### lacing.digest.VALUE_DIGEST_SCHEME *= 'lacing/annotation-value-digest/v1'*

Domain-separation tag mixed into [`annotation_value_digest()`](_autosummary/lacing.digest.html.md#lacing.digest.annotation_value_digest).

### lacing.digest.VALUE_FIELDS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('body', 'body_schema_uri', 'confidence', 'reference', 'tier')*

Annotation fields that constitute its *value*. See the module docstring for
why each is in, and why `id` and `provenance` are out.

### lacing.digest.annotation_body_digest(annotation)

Return the SHA-256 hex digest of `{body, body_schema_uri}` only.

The narrow sibling of [`annotation_value_digest()`](_autosummary/lacing.digest.html.md#lacing.digest.annotation_value_digest). It drops the
**entire** `reference` — *which asset* / *which node* / \*which
annotation\*, not merely *when* — plus `tier` and `confidence`. So the
same caption over two **different assets** digests identically here, as
does the same body asserted on two different tiers or at two different
confidences.

That is a correctness bug in any consumer that reads any of those. Reach
for it only when the consumer demonstrably depends on nothing but what the
annotation *says*; prefer [`annotation_value_digest()`](_autosummary/lacing.digest.html.md#lacing.digest.annotation_value_digest) otherwise.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> from uuid import uuid4
>>> from lacing import Annotation, MediaRef, Provenance
>>> from lacing import RationalTime, TimeInterval
>>> def over(asset):
...     return Annotation(
...         id=uuid4(), tier="words",
...         reference=MediaRef(
...             asset_id=asset,
...             interval=TimeInterval(RationalTime(0), RationalTime(24000)),
...         ),
...         body={"text": "hello"},
...         body_schema_uri="annot://schema/word/v1",
...         provenance=Provenance(
...             was_generated_by="agent:m@1",
...             was_attributed_to="thor",
...             generated_at_time=RationalTime.now(),
...         ),
...     )
```

Different **assets**, identical body digest — this is the footgun:

```pycon
>>> annotation_body_digest(over("sha256:interview")) == (
...     annotation_body_digest(over("sha256:broadcast"))
... )
True
>>> annotation_value_digest(over("sha256:interview")) == (
...     annotation_value_digest(over("sha256:broadcast"))
... )
False
```

```pycon
>>> from uuid import uuid4
>>> from lacing import Annotation, MediaRef, Provenance
>>> from lacing import RationalTime, TimeInterval
>>> def make(interval):
...     return Annotation(
...         id=uuid4(),
...         tier="words",
...         reference=MediaRef(asset_id="sha256:abc", interval=interval),
...         body={"text": "hello"},
...         body_schema_uri="annot://schema/word/v1",
...         provenance=Provenance(
...             was_generated_by="agent:m@1",
...             was_attributed_to="thor",
...             generated_at_time=RationalTime.now(),
...         ),
...     )
>>> early = make(TimeInterval(RationalTime(0), RationalTime(24000)))
>>> late = make(TimeInterval(RationalTime(24000), RationalTime(48000)))
>>> annotation_body_digest(early) == annotation_body_digest(late)
True
>>> annotation_value_digest(early) == annotation_value_digest(late)
False
```

### lacing.digest.annotation_value_digest(annotation)

Return the SHA-256 hex digest of `annotation`’s value.

Covers `body`, `body_schema_uri`, `tier`, `reference` and
`confidence`. Excludes `id` and `provenance`, so a regeneration that
produces identical content produces an identical digest.

Use this for freshness and early cutoff. For optimistic concurrency use
`lacing.server.etag.annotation_etag()` instead — two digests, two
jobs, and neither substitutes for the other.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> from uuid import uuid4
>>> from lacing import Annotation, MediaRef, Provenance
>>> from lacing import RationalTime, TimeInterval
>>> def make(**kw):
...     base = dict(
...         id=uuid4(),
...         tier="words",
...         reference=MediaRef(
...             asset_id="sha256:abc",
...             interval=TimeInterval(RationalTime(0), RationalTime(24000)),
...         ),
...         body={"text": "hello"},
...         body_schema_uri="annot://schema/word/v1",
...         provenance=Provenance(
...             was_generated_by="agent:m@1",
...             was_attributed_to="thor",
...             generated_at_time=RationalTime.now(),
...         ),
...     )
...     base.update(kw)
...     return Annotation(**base)
```

A regeneration — new `id`, new timestamp, same content — digests the same:

```pycon
>>> a = make()
>>> b = make(provenance=Provenance(
...     was_generated_by="agent:m@1",
...     was_attributed_to="thor",
...     generated_at_time=RationalTime(999),
... ))
>>> annotation_value_digest(a) == annotation_value_digest(b)
True
```

A changed body does not:

```pycon
>>> annotation_value_digest(make(body={"text": "goodbye"})) == (
...     annotation_value_digest(a)
... )
False
```


# _autosummary/lacing.exhibit.html.md

# lacing.exhibit

Artifact exhibit — render an annotation graph as a readable document.

Walks a set of [`Annotation`](_autosummary/lacing.html.md#lacing.Annotation)s and lays them out as a
document: each artifact is a card with its body, any generated images,
and **in-document hyperlinks** to the artifacts it was derived from and
the ones it feeds into.

Images are written once as content-addressed sibling files under an
`images/` directory and referenced relatively, so the HTML and
Markdown stay small. The PDF is still self-contained — weasyprint
resolves the relative paths (via `base_url`) and embeds the bytes.

The annotation graph — annotations + `provenance.was_derived_from` +
image references — *is* the “artifacts and links” model, so this runs on
**any** lacing graph: a project, a test run’s artifacts, anything. The
renderer is pure (graph → document) and has no knowledge of where the
graph came from.

HTML is the authored format; the PDF and Markdown derive from it so the
three never drift:

- **PDF** via `weasyprint` — chosen over wkhtmltopdf because it is the
  HTML→PDF engine that carries the in-document anchor links through to
  **clickable PDF links** (wkhtmltopdf renders them as broken external
  URIs).
- **Markdown** via `dn.html_to_markdown`.

Both derivations are optional — `pip install lacing[exhibit]`. A
missing converter raises an informative error naming the install
command, rather than silently dropping the format.

Images are resolved through an injected `image_resolver` callback
(image-reference → local file path) so this module needs no knowledge
of how media is stored or downloaded — the caller wires that in.

### Functions

| [`render_artifact_exhibit`](_autosummary/lacing.exhibit.html.md#lacing.exhibit.render_artifact_exhibit)(annotations, \*, out_dir)   | Render an annotation graph as a human-readable artifact exhibit.   |
|------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|

### lacing.exhibit.render_artifact_exhibit(annotations, , out_dir, formats=('html', 'pdf', 'md'), title='Artifact exhibit', image_resolver=None)

Render an annotation graph as a human-readable artifact exhibit.

Lays every annotation out as a card — body, panel images, and
in-document hyperlinks to the artifacts it derives from / feeds
into. **HTML is authored**; the PDF and Markdown derive from it.

Panel images are written once as content-addressed sibling files
under `<out_dir>/images/` and referenced relatively, so the HTML
and Markdown stay small; the PDF embeds them and stays a single
self-contained file. The `images/` directory is created only when
the graph actually has images.

* **Parameters:**
  * **annotations** ([`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)) – the lacing annotations to exhibit (any iterable).
    Order is preserved — pass them chain-ordered for a document
    that reads top-to-bottom.
  * **out_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – directory the `exhibit.{html,pdf,md}` files land in.
  * **formats** ([`Sequence`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – which of `html` / `pdf` / `md` to write. The HTML
    is always built in memory (the others derive from it).
  * **title** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – document title.
  * **image_resolver** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)], [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]]) – `image-reference → local file path` callback.
    Defaults to reading the reference’s own `path` field; a
    caller whose images live behind URLs passes a resolver that
    downloads / caches them (keeping this module media-agnostic).
* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]
* **Returns:**
  The written file paths.
* **Raises:**
  [**RuntimeError**](https://docs.python.org/3/builtins/exceptions.html#RuntimeError) – when `pdf` / `md` is requested but its optional
      converter (`weasyprint` / `dn`) is not installed — the
      message names the install command.


# _autosummary/lacing.html.md

# lacing

lacing — interval annotation system.

Standoff, interval-keyed annotations with rational time, ELAN tier
stereotypes, Allen’s interval algebra, and a `MutableMapping` facade.

Quick start:

```pycon
>>> from lacing import RationalTime, TimeInterval, Annotation, MemoryStore
>>> # Load a TextGrid, query overlaps, save as WebVTT — see misc/docs/.
```

Read `CLAUDE.md` and `misc/docs/Lacing Development Roadmap.md` for the
full story. `.claude/skills/` contains the rules.

### Functions

| [`hash_bytes`](_autosummary/lacing.html.md#lacing.hash_bytes)(data)                                  | Return the canonical `asset_id` (SHA-256 hex) for `data`.               |
|----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`hash_file`](_autosummary/lacing.html.md#lacing.hash_file)(path, \*[, chunk_size])                 | Return the canonical `asset_id` (SHA-256 hex) for the file at `path`.   |
| [`annotation_value_digest`](_autosummary/lacing.html.md#lacing.annotation_value_digest)(annotation)               | Return the SHA-256 hex digest of `annotation`'s value.                  |
| [`annotation_body_digest`](_autosummary/lacing.html.md#lacing.annotation_body_digest)(annotation)                | Return the SHA-256 hex digest of `{body, body_schema_uri}` only.        |
| [`register_store_migration`](_autosummary/lacing.html.md#lacing.register_store_migration)(\*, store_kind, ...)     | Register a forward store migration from `from_version` to `to_version`. |
| [`migrate_annot_file`](_autosummary/lacing.html.md#lacing.migrate_annot_file)(path, \*[, to_version])        | Migrate a `.annot` file in place, returning `(from, to)` versions.      |
| [`replay_oplog`](_autosummary/lacing.html.md#lacing.replay_oplog)(log, \*[, until_clock, ...])         | Rebuild a store by replaying `log` up to (and including) `until_clock`. |
| [`register_processor`](_autosummary/lacing.html.md#lacing.register_processor)([func, name])                  | Register a processor under `name` (defaults to the function name).      |
| [`registered_processors`](_autosummary/lacing.html.md#lacing.registered_processors)()                           | Names of every registered processor, sorted.                            |
| [`run_processor_async`](_autosummary/lacing.html.md#lacing.run_processor_async)(name, \*, store, oplog, ...)  | Run a processor in the current event loop.                              |
| [`run_processor_sync`](_autosummary/lacing.html.md#lacing.run_processor_sync)(name, \*, store, oplog, ...)   | Run a processor synchronously and return its result.                    |
| [`get_tracer`](_autosummary/lacing.html.md#lacing.get_tracer)([name, version])                       | Return a tracer or a no-op fallback.                                    |
| [`maybe_span`](_autosummary/lacing.html.md#lacing.maybe_span)(tracer, name, \*\*attributes)          | Open a span on `tracer`, attaching `attributes` if supported.           |
| [`traced`](_autosummary/lacing.html.md#lacing.traced)(tracer[, span_name, record_args])          | Decorator: wrap a function in a span on `tracer`.                       |
| [`instrument_otel`](_autosummary/lacing.html.md#lacing.instrument_otel)(app, \*[, tracer_name])           | Add OpenTelemetry instrumentation to a FastAPI app.                     |
| [`is_otel_active`](_autosummary/lacing.html.md#lacing.is_otel_active)()                                  | Quick check: is OTel installed AND a TracerProvider configured?         |
| [`cohen_kappa`](_autosummary/lacing.html.md#lacing.cohen_kappa)(a, b)                                 | Cohen's kappa for two annotators on a categorical label.                |
| [`krippendorff_alpha`](_autosummary/lacing.html.md#lacing.krippendorff_alpha)(annotations, \*[, distance])   | Krippendorff's α across any number of annotators.                       |
| [`interval_iou`](_autosummary/lacing.html.md#lacing.interval_iou)(a, b)                                | Intersection-over-Union for two time intervals.                         |
| [`boundary_iou`](_autosummary/lacing.html.md#lacing.boundary_iou)(a, b)                                | Mean IoU between two sets of intervals via greedy best-match.           |
| [`register_body_schema`](_autosummary/lacing.html.md#lacing.register_body_schema)(uri, model)                  | Register `model` as the validator for `uri`.                            |
| [`register_migration`](_autosummary/lacing.html.md#lacing.register_migration)(\*, schema_name, ...)          | Register a forward migration from `v<from_version>` to `v<to_version>`. |
| [`json_schema`](_autosummary/lacing.html.md#lacing.json_schema)(uri)                                  | Return the JSON Schema for the body model registered at `uri`.          |
| [`export_json_schemas`](_autosummary/lacing.html.md#lacing.export_json_schemas)(target_dir, \*[, ...])        | Write every registered schema as JSON files under `target_dir`.         |
| [`render_artifact_exhibit`](_autosummary/lacing.html.md#lacing.render_artifact_exhibit)(annotations, \*, out_dir) | Render an annotation graph as a human-readable artifact exhibit.        |
| [`migrate`](_autosummary/lacing.html.md#lacing.migrate)(body, \*, from_uri, to_uri)               | Migrate `body` from `from_uri` to `to_uri` via registered steps.        |
| [`validate_body`](_autosummary/lacing.html.md#lacing.validate_body)(body, uri)                          | Validate `body` against the schema registered for `uri`.                |

### Classes

| [`RationalTime`](_autosummary/lacing.html.md#lacing.RationalTime)(value[, rate])                      | A point in time as `value / rate` seconds.                                     |
|---------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| [`TimeInterval`](_autosummary/lacing.html.md#lacing.TimeInterval)(start, end)                         | A half-open interval `[start, end)`.                                           |
| [`Tier`](_autosummary/lacing.html.md#lacing.Tier)(name, \*[, stereotype, parent, metadata])   | A named annotation layer with optional parent and stereotype.                  |
| [`TierStereotype`](_autosummary/lacing.html.md#lacing.TierStereotype)(\*values)                         | Constraints on how a child tier relates to its parent tier.                    |
| [`Annotation`](_autosummary/lacing.html.md#lacing.Annotation)(\*\*data)                             | The single annotation envelope.                                                |
| [`MediaRef`](_autosummary/lacing.html.md#lacing.MediaRef)(\*\*data)                               | Reference to a region of a content-addressed media asset.                      |
| [`NodeRef`](_autosummary/lacing.html.md#lacing.NodeRef)(\*\*data)                                | Reference to a node in a structured scene/document graph.                      |
| [`AnnotationRef`](_autosummary/lacing.html.md#lacing.AnnotationRef)(\*\*data)                          | Reference to another annotation (for discussion threads, review, derivations). |
| [`Provenance`](_autosummary/lacing.html.md#lacing.Provenance)(\*\*data)                             | W3C PROV-O subset, embedded inline on every annotation.                        |
| [`Artifact`](_autosummary/lacing.html.md#lacing.Artifact)(\*\*data)                               | A content-addressed generated file with provenance.                            |
| [`Rights`](_autosummary/lacing.html.md#lacing.Rights)(\*\*data)                                 | Who owns the bytes of an *acquired* artifact, and on what terms.               |
| [`ArtifactStore`](_autosummary/lacing.html.md#lacing.ArtifactStore)(catalog[, blobs])                  | Facade over an artifact `catalog` and an optional `blobs` store.               |
| [`AllenRelation`](_autosummary/lacing.html.md#lacing.AllenRelation)(\*values)                          | The thirteen Allen relations.                                                  |
| [`IntervalAnnotationStore`](_autosummary/lacing.html.md#lacing.IntervalAnnotationStore)(\*args, \*\*kwargs)      | Protocol for any interval-keyed annotation store.                              |
| [`MemoryStore`](_autosummary/lacing.html.md#lacing.MemoryStore)()                                    | `IntervalAnnotationStore` implementation over `intervaltree`.                  |
| [`MappingStore`](_autosummary/lacing.html.md#lacing.MappingStore)(mapping, \*[, codec, tiers_key])    | An `IntervalAnnotationStore` persisted to an injected `MutableMapping`.        |
| [`SqliteStore`](_autosummary/lacing.html.md#lacing.SqliteStore)(path, \*[, check_same_thread, ...])  | SQLite-backed `IntervalAnnotationStore`.                                       |
| [`OpLog`](_autosummary/lacing.html.md#lacing.OpLog)(\*args, \*\*kwargs)                        | Append-only log of mutations.                                                  |
| [`OpLogEntry`](_autosummary/lacing.html.md#lacing.OpLogEntry)(clock, operation, target_id, payload) | One row of the op-log.                                                         |
| [`InMemoryOpLog`](_autosummary/lacing.html.md#lacing.InMemoryOpLog)()                                  | Simple list-backed op-log.                                                     |
| [`SqliteOpLog`](_autosummary/lacing.html.md#lacing.SqliteOpLog)(path, \*[, check_same_thread])       | Op-log backed by a SQLite table.                                               |

### Exceptions

| [`LossyTimeConversionError`](_autosummary/lacing.html.md#lacing.LossyTimeConversionError)   | Raised when a rate or seconds conversion would lose precision.           |
|-----------------------------------------------------------------------------|--------------------------------------------------------------------------|
| [`NonStringBodyKeyError`](_autosummary/lacing.html.md#lacing.NonStringBodyKeyError)      | An annotation `body` contains a mapping key that is not a `str`.         |
| [`SchemaMismatchError`](_autosummary/lacing.html.md#lacing.SchemaMismatchError)        | Raised when opening a `.annot` file with an incompatible schema.         |
| [`StoreMigrationError`](_autosummary/lacing.html.md#lacing.StoreMigrationError)        | Raised when a store migration step is missing or fails.                  |
| [`ProcessorError`](_autosummary/lacing.html.md#lacing.ProcessorError)             | Raised when a registered processor's invocation fails.                   |
| [`BodySchemaError`](_autosummary/lacing.html.md#lacing.BodySchemaError)            | Raised when a body fails validation against its registered schema.       |
| [`EmptySchemaRegistryError`](_autosummary/lacing.html.md#lacing.EmptySchemaRegistryError)   | Raised when an export would write an empty registry over real artifacts. |
| [`UnknownBodySchemaError`](_autosummary/lacing.html.md#lacing.UnknownBodySchemaError)     | Raised when an annotation's body_schema_uri has no registered model.     |
| [`MigrationError`](_autosummary/lacing.html.md#lacing.MigrationError)             | Raised when a migration step is missing or fails.                        |

### *class* lacing.AllenRelation(\*values)

Bases: [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Enum`](https://docs.python.org/3/library/enum.html#enum.Enum)

The thirteen Allen relations.

Symbols match Allen (1983); inverse pairs end in `i`.

#### inverse()

The inverse relation.

* **Return type:**
  [`AllenRelation`](_autosummary/lacing.allen.html.md#lacing.allen.AllenRelation)

### *class* lacing.Annotation(\*\*data)

Bases: `SchemaModel`

The single annotation envelope. `body` is typed by `body_schema_uri`.

#### *property* interval *: [TimeInterval](_autosummary/lacing.time.html.md#lacing.time.TimeInterval) | [None](https://docs.python.org/3/builtins/constants.html#None)*

the reference’s interval, if any.

* **Type:**
  Convenience

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* lacing.AnnotationRef(\*\*data)

Bases: `SchemaModel`

Reference to another annotation (for discussion threads, review, derivations).

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* lacing.Artifact(\*\*data)

Bases: `BaseModel`

A content-addressed generated file with provenance.

`asset_id` is the SHA-256 hex digest of the artifact’s bytes. Two
artifacts with the same `asset_id` are byte-identical regardless of
where they live — so caches keyed on `asset_id` are safe across
machines and re-runs.

`provenance` reuses [`lacing.Provenance`](_autosummary/lacing.html.md#lacing.Provenance) so the lineage chain
(`was_derived_from`, `was_generated_by`) is the same for artifacts
and annotations. An annotation referencing an artifact does so via
`MediaRef(asset_id=artifact.asset_id, …)`.

#### *classmethod* from_bytes(data, , kind, was_generated_by, was_attributed_to, path=None, url=None, was_derived_from=(), activity='create', generated_at_time=None, duration_s=None, mime=None, cost_usd=None, producer_call_id=None, rights=None)

Create an Artifact from in-memory bytes.

* **Return type:**
  [`Artifact`](_autosummary/lacing.artifact.html.md#lacing.artifact.Artifact)

#### *classmethod* from_path(path, , kind, was_generated_by, was_attributed_to, was_derived_from=(), activity='create', generated_at_time=None, duration_s=None, mime=None, cost_usd=None, producer_call_id=None, rights=None)

Create an Artifact from a local file. Hashes the file’s bytes.

* **Return type:**
  [`Artifact`](_autosummary/lacing.artifact.html.md#lacing.artifact.Artifact)

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

#### to_media_ref(interval)

Return a [`lacing.MediaRef`](_autosummary/lacing.html.md#lacing.MediaRef) pointing at this artifact.

Use this to attach an annotation to a region of the artifact:
`MediaRef(asset_id=artifact.asset_id, interval=…)`.

* **Return type:**
  MediaRef

### *class* lacing.ArtifactStore(catalog, blobs=None)

Bases: [`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)

Facade over an artifact `catalog` and an optional `blobs` store.

The object *is* a `MutableMapping[str, record]` over the catalog —
`store[artifact_id]`, iteration, `len`, `get`, `clear` and the
rest of the mapping surface all act on artifact **metadata records**. The
heavier byte operations ([`put_blob()`](_autosummary/lacing.html.md#lacing.ArtifactStore.put_blob), [`get_blob()`](_autosummary/lacing.html.md#lacing.ArtifactStore.get_blob),
[`has_blob()`](_autosummary/lacing.html.md#lacing.ArtifactStore.has_blob)) are rich methods that are deliberately *not* squeezed
into the mapping protocol.

* **Parameters:**
  * **catalog** ([`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), `BaseModel`]) – Injected `id -> record` store. Records are pydantic models
    (`lacing.Artifact` by default, but any `BaseModel` works — the
    store does not inspect the record’s shape).
  * **blobs** ([`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Injected `content_hash -> bytes` store, or `None` for a
    catalog-only store (Stage-1 metadata persistence). Blob methods
    raise / no-op when it is `None`. This is the *raw* backend:
    on a filesystem backend its own `del` and iteration do no
    containment check, so go through [`delete_blob()`](_autosummary/lacing.html.md#lacing.ArtifactStore.delete_blob) and
    [`iter_blobs()`](_autosummary/lacing.html.md#lacing.ArtifactStore.iter_blobs) (and the read methods) rather than touching
    `store.blobs` directly (lacing#55).

Construct one with [`in_memory()`](_autosummary/lacing.html.md#lacing.ArtifactStore.in_memory) or [`from_directory()`](_autosummary/lacing.html.md#lacing.ArtifactStore.from_directory) rather than
wiring the backing stores by hand, unless you are injecting a custom
backend.

#### blob_location(content_hash)

Resolve the cheapest *servable* location for a blob — without
reading its bytes. The capability the HTTP layer probes to serve a
blob the most efficient way, generalizing [`blob_path()`](_autosummary/lacing.html.md#lacing.ArtifactStore.blob_path) so
filesystem / S3 / R2 backends all answer one probe:

- **object-store** backends (S3/R2) that expose a presigned-URL
  capability — a `url_for(content_hash)` callable — return a \*\*URL
  string\*\*, so the caller can 302-redirect and let the object store
  serve the bytes (and HTTP `Range`) directly, off the app process;
- **filesystem** backends return a local **Path** (see
  [`blob_path()`](_autosummary/lacing.html.md#lacing.ArtifactStore.blob_path)) so the caller hands the OS the file (Range free);
- everything else (plain `dict`, or a missing blob) returns
  `None` — the cue to fall back to [`iter_blob()`](_autosummary/lacing.html.md#lacing.ArtifactStore.iter_blob) streaming.

Callers treat `None` as “stream it”, not as an error.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### blob_path(content_hash)

Return the local filesystem path of the blob, or `None`.

The store’s blob backend is opaque (any `MutableMapping`), but some
backends — notably the filesystem-backed `dol.Files` produced by
[`from_directory()`](_autosummary/lacing.html.md#lacing.ArtifactStore.from_directory) — store each blob as one file under a known
root directory. This method exposes that path *when available*, so a
caller (e.g. a FastAPI route serving video) can hand the OS the file
descriptor and let it answer HTTP `Range` requests directly. It
returns `None` for:

- blob stores without a `rootdir` attribute (e.g. plain `dict`,
  object-store backends — callers should fall back to
  [`iter_blob()`](_autosummary/lacing.html.md#lacing.ArtifactStore.iter_blob));
- blobs that are not present.
- a `content_hash` that does not resolve to a path *inside*
  `rootdir` (path traversal, e.g. `"../../etc/passwd"` or an
  absolute path, or a same-named symlink pointing outside the
  store) — refused rather than served (lacing#50).

Callers must treat `None` as the cue to use the streaming read
path, not as an error. Deleting and listing blobs have contained
counterparts too ([`delete_blob()`](_autosummary/lacing.html.md#lacing.ArtifactStore.delete_blob), [`iter_blobs()`](_autosummary/lacing.html.md#lacing.ArtifactStore.iter_blobs)), and the
[`from_directory()`](_autosummary/lacing.html.md#lacing.ArtifactStore.from_directory) catalog confines artifact ids the same way
(lacing#55).

The containment check is *point-in-time*: the returned path is the
fully resolved, symlink-free location of a regular file that was
inside `rootdir` when checked. The caller opens it later, by name,
so if untrusted parties can write into `rootdir` itself, open it
with `O_NOFOLLOW` — or serve via [`iter_blob()`](_autosummary/lacing.html.md#lacing.ArtifactStore.iter_blob), which reads
through a descriptor and has no such window.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### count_refs(content_hash)

How many catalog records point at `content_hash` (a blob).

The reference count that a garbage collector needs: a blob is safe to
delete only when **no** catalog record still names its content hash.
Content-addressed blobs are deduplicated, so two artifacts can share
one blob — deleting the blob the moment one of them goes away would
orphan the other.

This is the *probe* half of a capability pair (mirroring
[`blob_location()`](_autosummary/lacing.html.md#lacing.ArtifactStore.blob_location) probing a blob store for `url_for`): a catalog
backend that can answer the count cheaply — the SQL catalog from
[`from_sql()`](_autosummary/lacing.html.md#lacing.ArtifactStore.from_sql), via an indexed `content_hash` column — exposes a
`refcount_by_content_hash` callable, and this method uses it so the
count is **one indexed query, not a full catalog scan**. Any other
catalog (dict, `Files`) falls back to scanning every record. The
facade is unchanged either way; only the speed differs.

* **Parameters:**
  **content_hash** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The blob’s content hash (`Artifact.asset_id`).
* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)
* **Returns:**
  The number of catalog records whose content hash equals
  `content_hash`.

#### delete_blob(content_hash)

Delete the blob `content_hash`; `KeyError` if there is none.

The contained counterpart of `del store.blobs[content_hash]`
(lacing#55). On a filesystem-backed store (one exposing `rootdir`)
a key is deleted only if [`has_blob()`](_autosummary/lacing.html.md#lacing.ArtifactStore.has_blob) would say it exists — a
regular file that resolves inside `rootdir` (see
`_ContainedDirStore`, the same gate the catalog of
[`from_directory()`](_autosummary/lacing.html.md#lacing.ArtifactStore.from_directory) uses). A key that escapes (`..`, an absolute path, a
NUL, a symlink pointing outside) raises `KeyError` exactly like a
missing blob, as does any key with a `..` segment, and nothing
outside the root is touched. The deletion itself is delegated to the
backend’s own `__delitem__` on the key as given (normalised), so the
backend’s delete policy (e.g. `dol.Files` moving files to the
trash) is unchanged, and deleting an in-root symlink removes the
link, never the blob it points at.

The check is point-in-time, like [`blob_path()`](_autosummary/lacing.html.md#lacing.ArtifactStore.blob_path): a party that can
write into `rootdir` and swap a *directory* component of a nested
key for a symlink between the check and the delete can still redirect
it. Flat content-hash keys, which is all this store writes, have no
such component.

Backends without a `rootdir` (`dict`, object stores) have no
filesystem to escape; the key passes through to their `del`.

* **Raises:**
  [**KeyError**](https://docs.python.org/3/builtins/exceptions.html#KeyError) – no such blob, the key escapes the root, or no blob store
      is configured.
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### *classmethod* from_aws(bucket_name, uri, \*, record_type=<class 'lacing.artifact.Artifact'>, collection_name='artifact_catalog', prefix=None, s3_kwargs=None, sql_kwargs=None)

The production pairing: **S3-compatible blobs + SQL catalog**.

A thin convenience over [`from_s3()`](_autosummary/lacing.html.md#lacing.ArtifactStore.from_s3) (blobs) and [`from_sql()`](_autosummary/lacing.html.md#lacing.ArtifactStore.from_sql)
(catalog) so the common cloud deployment is one call. The blob store is
an `S3Store` (AWS S3 / Cloudflare R2 / MinIO / Supabase); the catalog
is the durable, queryable SQL table — Postgres in production, SQLite for
a smoke test. Vendor specifics live only in the two kwargs dicts.

* **Parameters:**
  * **bucket_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Object-store bucket for the content-addressed blobs.
  * **uri** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – SQLAlchemy URI for the catalog (`postgresql://…` in prod).
  * **record_type** ([`type`](https://docs.python.org/3/builtins/functions.html#type)[`BaseModel`]) – Pydantic model the catalog (de)serializes.
  * **collection_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – SQL table name for the catalog.
  * **prefix** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Optional key prefix within the bucket.
  * **s3_kwargs** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Extra kwargs forwarded to [`from_s3()`](_autosummary/lacing.html.md#lacing.ArtifactStore.from_s3) (credentials,
    `endpoint_url` for R2/MinIO/Supabase, `region_name`, …).
  * **sql_kwargs** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Extra kwargs forwarded to [`from_sql()`](_autosummary/lacing.html.md#lacing.ArtifactStore.from_sql)
    (`content_hash_of`, SQLAlchemy engine kwargs, …).
* **Return type:**
  [`ArtifactStore`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore)

#### *classmethod* from_directory(root, \*, record_type=<class 'lacing.artifact.Artifact'>)

An ArtifactStore persisted under `root`.

Lays out two subdirectories: `catalog/` (one `<id>.json` file per
record) and `blobs/` (one file per content hash). Both are `dol`
filesystem stores, so the same facade works unchanged over any other
`dol` backend (object storage, etc.) when injected directly.

Artifact ids are confined to `catalog/` (lacing#55): an id whose
`<id>.json` would resolve outside it — `..` segments, an absolute
path, a NUL, or a symlink pointing out — is refused with `KeyError`
on get, save and delete, and is simply not `in` the store. Nested
ids (`"a/b"`) that stay inside keep working, and listing never
follows a symlinked directory out of `catalog/`. See
`_ContainedDirStore`.

* **Parameters:**
  * **root** ([`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Directory to hold the store. Created if missing.
  * **record_type** ([`type`](https://docs.python.org/3/builtins/functions.html#type)[`BaseModel`]) – The pydantic model the catalog deserializes JSON into.
    Defaults to [`Artifact`](_autosummary/lacing.artifact.html.md#lacing.artifact.Artifact); callers with
    their own record schema pass their model here.
* **Return type:**
  [`ArtifactStore`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore)

#### *classmethod* from_s3(bucket_name, , catalog=None, prefix=None, \*\*s3_kwargs)

An ArtifactStore with **blobs in an S3-compatible object store**
(AWS S3 / Cloudflare R2 / MinIO / Supabase) via `s3dol`.

Content-addressed blobs are a natural fit for object storage: flat,
immutable, dedup-friendly, forever-cacheable keys. [`blob_location()`](_autosummary/lacing.html.md#lacing.ArtifactStore.blob_location)
returns a presigned GET URL (via s3dol’s `url_for`) so a serving layer
can 302-redirect and let the store deliver the bytes (and HTTP `Range`)
directly, off the app process.

The blob and catalog backends are independent by design: the catalog
defaults to an in-memory dict here — swap in a durable catalog (e.g. a
Postgres-backed `MutableMapping`) for production.

* **Parameters:**
  * **bucket_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The object-store bucket.
  * **catalog** ([`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), `BaseModel`] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – The `id -> record` catalog `MutableMapping`. Defaults
    to `{}` (in-memory).
  * **prefix** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Optional key prefix within the bucket (e.g. `"blobs"`).
  * **\*\*s3_kwargs** – Forwarded to `s3dol.s3_store` — `endpoint_url`
    (set for R2 / MinIO / Supabase), `region_name`, `profile`,
    `credentials`, `preset`, `anon`, `on_missing_bucket`.
    The pre-v1 spellings (`aws_access_key_id` /
    `aws_secret_access_key` / `aws_session_token` /
    `profile_name` / `make_bucket`) are still accepted and
    translated, with a `DeprecationWarning`.
* **Return type:**
  [`ArtifactStore`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore)

Requires `s3dol>=1` (and `boto3`) — imported lazily so the
dependency is only needed when this constructor is used.

#### *classmethod* from_sql(uri, \*, blobs=None, record_type=<class 'lacing.artifact.Artifact'>, collection_name='artifact_catalog', content_hash_of=None, \*\*db_kwargs)

An ArtifactStore with a **SQL-backed catalog** (durable, queryable)
via `sqldol` — the SQL counterpart of [`from_s3()`](_autosummary/lacing.html.md#lacing.ArtifactStore.from_s3)’s object store.

The catalog row schema is deliberately minimal and **vendor-neutral**:
one `TEXT` column holds the record serialized exactly as
[`from_directory()`](_autosummary/lacing.html.md#lacing.ArtifactStore.from_directory) serializes it (`record.model_dump_json` out,
`record_type.model_validate_json` in), and one indexed `content_hash`
column carries the record’s blob hash so the GC reference count
([`count_refs()`](_autosummary/lacing.html.md#lacing.ArtifactStore.count_refs)) is a single indexed query rather than a full scan.

Because the connection is just a SQLAlchemy URI, the *same* code runs
on SQLite for tests and on Postgres in production — that is the whole
point of the facade. Pick the backend with the `uri` alone:

```default
ArtifactStore.from_sql("sqlite:///artifacts.db")          # local / tests
ArtifactStore.from_sql("postgresql://u:p@host:5432/db")   # production
```

The catalog and blob backends are independent: pass `blobs` to pair a
SQL catalog with any blob store (e.g. `from_s3`’s `S3Store`), or
leave it `None` for a catalog-only store (Stage-1 metadata
persistence; see [`from_aws()`](_autosummary/lacing.html.md#lacing.ArtifactStore.from_aws) for the common S3 + SQL pairing).

* **Parameters:**
  * **uri** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – SQLAlchemy connection URI (`sqlite:///…` or
    `postgresql://…`). The single knob that selects the vendor.
  * **blobs** ([`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Optional `content_hash -> bytes` blob store. `None` for a
    catalog-only store.
  * **record_type** ([`type`](https://docs.python.org/3/builtins/functions.html#type)[`BaseModel`]) – The pydantic model the catalog deserializes JSON into.
    Defaults to [`Artifact`](_autosummary/lacing.artifact.html.md#lacing.artifact.Artifact); callers with
    their own record schema pass their model here.
  * **collection_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The SQL table name for the catalog.
  * **content_hash_of** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[`BaseModel`], [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – How to read a record’s blob hash for the indexed
    `content_hash` column. Defaults to `asset_id` (the canonical
    Artifact field), then `content_hash`. Pass a callable for a
    record whose hash lives elsewhere; records without a hash store
    an empty string.
  * **\*\*db_kwargs** – Forwarded to `sqldol.SQLAlchemyStore` /
    SQLAlchemy’s `create_engine` (e.g. `connect_args`,
    `pool_size`).
* **Return type:**
  [`ArtifactStore`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore)

Requires `sqldol` (and `SQLAlchemy`) — imported lazily so the
dependency is only needed when this constructor is used.

#### get_blob(content_hash)

Return the bytes for `content_hash`, or `None` if absent.

On a filesystem-backed store (one exposing `rootdir`) the bytes are
read through `_open_within()`, never by re-opening the path by
name, so a key that would land outside `rootdir` — by `..`, an
absolute path, or a symlink, including one swapped in *after* the
containment check (lacing#50) — reads as absent.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### has_blob(content_hash)

Whether the blob store holds `content_hash`.

On a filesystem-backed store this is exactly “would [`get_blob()`](_autosummary/lacing.html.md#lacing.ArtifactStore.get_blob)
return bytes”: `False` for a key that resolves outside `rootdir`
or names anything but a regular file — see `_open_within()`.
Backends without a `rootdir` (`dict`, object stores) have no
filesystem to escape, so their keys pass through unfiltered.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

#### *classmethod* in_memory()

An ArtifactStore backed entirely by in-memory dicts.

For tests, scratch work, and as the trivial reference backend. Nothing
persists across processes.

* **Return type:**
  [`ArtifactStore`](_autosummary/lacing.artifact_store.html.md#lacing.artifact_store.ArtifactStore)

#### index()

Return the whole catalog as a plain dict (e.g. for UI hydration).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), `BaseModel`]

#### iter_blob(content_hash, , chunk_size=65536)

Yield the blob’s bytes in `chunk_size` chunks.

The streaming counterpart to [`get_blob()`](_autosummary/lacing.html.md#lacing.ArtifactStore.get_blob) — what an HTTP response
body iterates over when serving a large blob without holding it all
in process memory. The default implementation reads the whole blob
via [`get_blob()`](_autosummary/lacing.html.md#lacing.ArtifactStore.get_blob) and re-chunks it; a filesystem-backed store can
be swapped for a true streaming reader without changing this API.

* **Raises:**
  [**KeyError**](https://docs.python.org/3/builtins/exceptions.html#KeyError) – no blob exists for `content_hash`.
* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]

#### iter_blobs()

Yield the content hash (key) of every blob in the blob store.

The contained counterpart of `iter(store.blobs)` (lacing#55). On a
filesystem-backed store it yields exactly the keys [`has_blob()`](_autosummary/lacing.html.md#lacing.ArtifactStore.has_blob)
accepts — regular files inside `rootdir` — and never descends into
a symlinked directory (`dol.Files` does, so its listing can yield
files that live outside the root, or loop on a symlink cycle).
Hidden entries are skipped, as `dol.Files` skips them, which also
keeps the in-flight `.blob-*.part` spool files of
[`put_blob_stream()`](_autosummary/lacing.html.md#lacing.ArtifactStore.put_blob_stream) out of the listing.

Yields nothing when no blob store is configured; other backends are
iterated as they are.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

#### put_blob(data)

Store `data` content-addressed; return its content hash.

Idempotent: identical bytes always map to the same hash and overwrite
an identical blob. Delegates to [`put_blob_stream()`](_autosummary/lacing.html.md#lacing.ArtifactStore.put_blob_stream) so there is
exactly one write path — and therefore one atomicity story
(lacing#25).

* **Raises:**
  [**RuntimeError**](https://docs.python.org/3/builtins/exceptions.html#RuntimeError) – no blob store is configured.
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### put_blob_stream(chunks)

Stream `chunks` content-addressed; return their SHA-256 hash.

The streaming-friendly counterpart to [`put_blob()`](_autosummary/lacing.html.md#lacing.ArtifactStore.put_blob) — callers hand
in an iterable (e.g. `requests.Response.iter_content`) instead of
materializing the whole bytestring upfront. The hash is computed on
the fly.

When the blob store is filesystem-backed (exposes `rootdir`, as the
default directory store does), the bytes spool straight to a
same-directory tempfile and are renamed into place with
`os.replace` once the hash is known (lacing#25). Consequences a
caller may rely on:

- **peak memory is one chunk**, not 2× the payload — 100 MB videos
  flow through without a 200 MB spike;
- **a partial blob is never observable under its content address** —
  the name a reader could find only exists after the rename, which is
  atomic on POSIX and on Windows for same-directory renames — so
  `has_blob(h) == True` really does mean the full bytes are there;
- a failed or abandoned stream leaves nothing behind (the tempfile is
  unlinked).

Backends without a `rootdir` (plain `dict`, object-store
mappings) fall back to buffering the payload and assigning it whole —
their `__setitem__` is the atomicity story there.

* **Raises:**
  [**RuntimeError**](https://docs.python.org/3/builtins/exceptions.html#RuntimeError) – no blob store is configured.
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### save(artifact_id, record, , data=None)

Persist one artifact, optionally with its bytes.

Writes the blob (if `data` is given) **before** the catalog row, so a
crash never leaves the catalog pointing at missing bytes. Idempotent on
`artifact_id` — and, for the blob, on content — so retries are safe.

* **Parameters:**
  * **artifact_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The stable string id this artifact is filed under.
  * **record** (`BaseModel`) – The metadata record to store in the catalog.
  * **data** ([`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Optional raw bytes. When given, they are stored
    content-addressed and the content hash is returned; the caller
    is responsible for also recording that hash on `record`.
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)
* **Returns:**
  The blob’s content hash if `data` was written, else `None`.
* **Raises:**
  [**RuntimeError**](https://docs.python.org/3/builtins/exceptions.html#RuntimeError) – `data` was given but no blob store is configured.

### *exception* lacing.BodySchemaError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

Raised when a body fails validation against its registered schema.

### *exception* lacing.EmptySchemaRegistryError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised when an export would write an empty registry over real artifacts.

Almost always means the caller forgot `import lacing.bodies`: the
registry is populated by importing the body modules, so exporting without
that import silently truncates `index.json` to `{}` and leaves the
committed `v<N>.json` files orphaned.

### *class* lacing.InMemoryOpLog

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Simple list-backed op-log. Thread-safe via an RLock.

### *class* lacing.IntervalAnnotationStore(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

Protocol for any interval-keyed annotation store.

Conceptually a `MutableMapping[TimeInterval, list[Annotation]]`: keys
are `TimeInterval`; values are lists because multiple annotations can
share an interval (different tiers, multiple annotators, soft labels).

We use `Protocol` rather than inheriting from `MutableMapping` so
backends (in-memory, SQLite, Postgres) can structurally conform without
forcing a single class hierarchy. The mapping methods below match the
`MutableMapping` ABC; concrete backends like [`MemoryStore`](_autosummary/lacing.html.md#lacing.MemoryStore)
implement the full interface.

#### add(annotation)

Append `annotation` to the list at its reference interval.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### all()

Iterate every annotation in the store, order unspecified.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### at_tier(tier_name, query)

Annotations on `tier_name` that intersect `query`.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### by_tier(tier_name)

All annotations on `tier_name`, regardless of interval.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### contains(query)

Annotations whose interval strictly contains `query` (Allen `di`).

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### during(query)

Annotations whose interval is strictly inside `query` (Allen `d`).

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### equals(query)

Allen `=`: identical interval.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### extend(annotations)

Add many; equivalent to repeated `.add` but adapters can optimize.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### finishes(query)

Allen `f`: later start, same end.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### intersects(query)

Annotations whose interval shares any time with `query`.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### lock(key)

Exclusive lock named `key`, held for the `with` block.

Scoped to this store’s data (for `PostgresStore`, its owner and
project): holders of the same key exclude each other, different keys do
not. The reach of the exclusion is the backend’s: `MemoryStore`
excludes threads of this process, `SqliteStore` also other processes
on the host, `PostgresStore` every client of the database, on any
host. A holder that dies releases the lock. Not re-entrant.

* **Return type:**
  [`AbstractContextManager`](https://docs.python.org/3/library/contextlib.html#contextlib.AbstractContextManager)[[`None`](https://docs.python.org/3/builtins/constants.html#None)]

#### meets(query)

Allen `m`: `a.end == q.start`.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### overlaps(query)

Strict Allen `o`: `a.start < q.start < a.end < q.end`.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### relate(query, relations)

Annotations whose interval has any of the named `relations` to `query`.

Generic dispatch — useful when relations are computed at runtime.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### remove(annotation_id)

Remove and return the annotation with this id, or None if absent.

* **Return type:**
  [`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### starts(query)

Allen `s`: same start, earlier end.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### tiers()

Registered tiers. Annotations may reference tiers not yet registered;
callers decide whether that’s an error.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Tier`](_autosummary/lacing.tier.html.md#lacing.tier.Tier)]

#### update(annotation)

Replace the stored annotation that has `annotation.id`, atomically.

One commit: a concurrent reader sees the old annotation or the new one,
never neither (`remove(id)` then `add(...)` leaves a gap in
between). The replacement may move to a different interval or tier.
Raises `KeyError` when no annotation has that id — use [`add()`](_autosummary/lacing.html.md#lacing.IntervalAnnotationStore.add)
for a new one.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *exception* lacing.LossyTimeConversionError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

Raised when a rate or seconds conversion would lose precision.

### *class* lacing.MappingStore(mapping, \*, codec=(<function <lambda>>, <function <lambda>>), tiers_key='_\_lacing_tiers_\_')

Bases: [`MemoryStore`](_autosummary/lacing.store.memory.html.md#lacing.store.memory.MemoryStore)

An `IntervalAnnotationStore` persisted to an injected `MutableMapping`.

```pycon
>>> from lacing.store import MappingStore
>>> backing = {}
>>> store = MappingStore(backing)
>>> len(backing)  # nothing written until something is added
0
```

Reopening over the same mapping sees the same annotations and tiers.

Raises `ValueError` on a duplicate annotation id (as `SqliteStore`
does) and when an annotation is assigned under a key that is not its own
interval; both would otherwise not survive a reload.

#### close()

No-op. Writes go through immediately and the mapping is the caller’s.

Present so code that calls `store.close()` after each use (as it does
for `SqliteStore`) can treat both alike; the store stays usable.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* lacing.MediaRef(\*\*data)

Bases: `SchemaModel`

Reference to a region of a content-addressed media asset.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* lacing.MemoryStore

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

`IntervalAnnotationStore` implementation over `intervaltree`.

Conforms to the protocol in `lacing.store.base`. We don’t formally
inherit from `IntervalAnnotationStore` because it’s a `Protocol`
with method bodies — structural typing is enough.

#### lock(key)

Exclusive lock named `key` among the threads of this process.

* **Return type:**
  [`AbstractContextManager`](https://docs.python.org/3/library/contextlib.html#contextlib.AbstractContextManager)[[`None`](https://docs.python.org/3/builtins/constants.html#None)]

### *exception* lacing.MigrationError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised when a migration step is missing or fails.

### *class* lacing.NodeRef(\*\*data)

Bases: `SchemaModel`

Reference to a node in a structured scene/document graph.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *exception* lacing.NonStringBodyKeyError

Bases: [`TypeError`](https://docs.python.org/3/builtins/exceptions.html#TypeError)

An annotation `body` contains a mapping key that is not a `str`.

JSON object keys are strings, so `model_dump(mode="json")` coerces
non-string keys — and two distinct keys can coerce to the *same* string,
silently annihilating an entry. `{1: "a", "1": "b"}` dumps to
`{"1": "b"}`; a body differing only in the lost entry would digest
identically, which is a wrong cache **hit**.

Since lacing#24 the *envelope* refuses such a body at validation — the
producer-side fix. This error lives here rather than in `lacing.model`
because this module is deliberately import-light (stdlib only, pinned by
test) and the model can import from it, not vice versa.

### *class* lacing.OpLog(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

Append-only log of mutations.

Implementations must guarantee that the clock returned by
[`append()`](_autosummary/lacing.html.md#lacing.OpLog.append) is strictly greater than every previously-returned
clock value across the lifetime of the log.

#### append(operation, , target_id=None, payload=None, actor='anonymous')

Append an entry; return its assigned clock.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

#### entries(, until_clock=None, from_clock=None)

Iterate entries, optionally bounded by clock range (inclusive).

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`OpLogEntry`](_autosummary/lacing.oplog.html.md#lacing.oplog.OpLogEntry)]

#### latest_clock()

Highest clock currently in the log; 0 if empty.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

### *class* lacing.OpLogEntry(clock, operation, target_id, payload, actor='anonymous', received_at=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One row of the op-log.

#### actor *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

`user:<handle>` or `agent:<model>@<hash>` or `adapter:<format>`.

#### clock *: [int](https://docs.python.org/3/builtins/functions.html#int)*

Monotonic Lamport clock starting at 1. Strictly increasing per log.

#### operation *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

`add_annotation`, `remove_annotation`, `update_annotation`,
`add_tier`, `set_meta`, `import_batch`.

* **Type:**
  One of

#### payload *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]*

JSON-serializable payload sufficient to replay the operation.

#### received_at *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Wall-clock time the operation was received (seconds since epoch).

#### target_id *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Annotation id, tier name, meta key, or None for batch ops.

### *exception* lacing.ProcessorError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised when a registered processor’s invocation fails.

### *class* lacing.Provenance(\*\*data)

Bases: `SchemaModel`

W3C PROV-O subset, embedded inline on every annotation.

#### *property* generated_at_is_known *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

`False` when `generated_at_time` is the `UNKNOWN_GENERATED_AT` sentinel.

The one test every freshness or ordering consumer should make before
comparing `generated_at_time` values: an unknown time cannot be
ordered against a known one, and a consumer that compares anyway
reads the row as older than everything (lacing#44).

```pycon
>>> from lacing.time import RationalTime
>>> kw = dict(was_generated_by="user:x", was_attributed_to="x")
>>> Provenance(generated_at_time=RationalTime(0, 1000), **kw).generated_at_is_known
False
>>> Provenance(generated_at_time=RationalTime.now(), **kw).generated_at_is_known
True
```

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* lacing.RationalTime(value, rate=24000)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A point in time as `value / rate` seconds.

Immutable. Two `RationalTime` values with different rates compare via
their rational value, so `RationalTime(24, 24) == RationalTime(1, 1)`.

### Examples

```pycon
>>> RationalTime(24000) == RationalTime(1, 1)
True
>>> RationalTime.from_seconds("1.5", rate=2).value
3
```

#### *classmethod* from_seconds(seconds, rate=24000)

Build from seconds. Quantizes to `rate`; raises if lossy.

`seconds` may be a `str` like `"1.001"` to avoid float ingestion.

* **Return type:**
  [`RationalTime`](_autosummary/lacing.time.html.md#lacing.time.RationalTime)

#### *classmethod* from_seconds_lossy(seconds, , rate=24000, mode='round')

Build from seconds, quantizing to the nearest sample at `rate`.

Unlike [`from_seconds()`](_autosummary/lacing.html.md#lacing.RationalTime.from_seconds) — which raises
[`LossyTimeConversionError`](_autosummary/lacing.html.md#lacing.LossyTimeConversionError) when the value cannot be
represented exactly — this method always succeeds by quantizing.
`mode` selects the rounding rule:

- `"round"` — nearest sample, ties to even (default)
- `"floor"` — largest sample <= `seconds`
- `"ceil"`  — smallest sample >= `seconds`

Use this when sample-level quantization is knowingly acceptable —
the common case for user-supplied durations. Use [`from_seconds()`](_autosummary/lacing.html.md#lacing.RationalTime.from_seconds)
when exactness matters and a lossy conversion should be an error.

### Examples

```pycon
>>> RationalTime.from_seconds_lossy("0.1", rate=3).value
0
>>> RationalTime.from_seconds_lossy("0.1", rate=3, mode="ceil").value
1
```

* **Return type:**
  [`RationalTime`](_autosummary/lacing.time.html.md#lacing.time.RationalTime)

#### *classmethod* from_wire(d)

Build from the wire form. Unknown keys are an error, not ignored.

`RATIONAL_TIME_JSON_SCHEMA` sets `additionalProperties: false` and
lacing-ui’s Zod mirror is `.strict()`; accepting extras here would
make Python the one lax end of a contract both other ends enforce, so
a `{"v": 0, "r": 1, "seconds": 0.0}` payload would round-trip through
Python and then be rejected by the frontend.

* **Return type:**
  [`RationalTime`](_autosummary/lacing.time.html.md#lacing.time.RationalTime)

#### *classmethod* now(rate=24000)

Wall-clock time as a [`RationalTime`](_autosummary/lacing.html.md#lacing.RationalTime), quantized to `rate`.

Uses `time.time_ns()` and builds the value directly, sidestepping
the float-quantization landmine of `from_seconds(float)`. Every
producer of an annotation or artifact needs this for
`Provenance.generated_at_time` — the only value that field reads
as a *known* time. Tick 0 there is the UNKNOWN sentinel
(`lacing.model.UNKNOWN_GENERATED_AT`), never the epoch.

* **Return type:**
  [`RationalTime`](_autosummary/lacing.time.html.md#lacing.time.RationalTime)

#### to_rate(new_rate)

Re-express at `new_rate`. Raises `LossyTimeConversionError` on loss.

* **Return type:**
  [`RationalTime`](_autosummary/lacing.time.html.md#lacing.time.RationalTime)

#### to_seconds()

Float seconds — for display only. Never round-trip through this.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

#### *classmethod* zero(rate=24000)

Tick 0 at `rate` — the start of a media timeline.

Not a wall-clock timestamp: as a `Provenance.generated_at_time` it
is the UNKNOWN sentinel (`lacing.model.UNKNOWN_GENERATED_AT`), not
the epoch. Producers stamp [`now()`](_autosummary/lacing.html.md#lacing.RationalTime.now) there.

* **Return type:**
  [`RationalTime`](_autosummary/lacing.time.html.md#lacing.time.RationalTime)

### *class* lacing.Rights(\*\*data)

Bases: `BaseModel`

Who owns the bytes of an *acquired* artifact, and on what terms.

The fields a Creative-Commons-family licence needs to discharge its
attribution duty (title, author, source, licence: “TASL”), named
literal-for-literal after the two records the federation already keeps —
`illustration.ImageResult` and `an.ir.assets.AssetSource` — so both map
onto this one without a rename table (`provider`, `id`, `license`,
`license_url`, `attribution`, `source_page_url`, `author`,
`author_url`, `cacheable`).

Semantics worth the trap they avoid:

- **\`\`Artifact.rights is None\`\` means “we made this”**, not “unknown”. An
  acquired artifact whose terms nobody recorded is a `Rights` with only
  `provider` set.
- **\`\`license is None\`\` means UNKNOWN, never unencumbered.** Free of
  obligations is stated, e.g. `license="cc0-1.0"`.
- **\`\`cacheable is None\`\` means not stated**; `False` forbids keeping the
  bytes, `True` permits it.
- `license` is an SPDX id (`"CC-BY-4.0"`) or a provider’s own code
  (`"by-sa"`); lacing records it verbatim and never classifies it
  (classification lives with the consumer, e.g. `an`’s licence classes).
- The record describes **this artifact’s own bytes**. A render that embeds
  a third-party image carries no `Rights` of its own; its obligations
  travel through `provenance.was_derived_from` and are rolled up by the
  consumer. `rights is None` on a derivative is *not* a clearance.

`provider` is required, so an empty `Rights()` cannot exist: an
artifact either has no record (we made it) or says where it came from.
Text fields are `None` or non-blank; a blank string is refused.

Caveat: an artifact catalog is keyed by `asset_id` (the content hash), so
two records for the *same bytes* are one row and the last write wins. A
rights-less record written after an acquired one with identical bytes
replaces it, rights included. Writers that re-record bytes they already
hold must carry the existing `rights` forward.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *exception* lacing.SchemaMismatchError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised when opening a `.annot` file with an incompatible schema.

### *class* lacing.SqliteOpLog(path, , check_same_thread=True)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Op-log backed by a SQLite table.

Designed to share a database file with `SqliteStore` so the store
snapshot + the op-log live together and survive a single
`cp project.annot project.backup` step.

### *class* lacing.SqliteStore(path, , check_same_thread=True, migrate=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

SQLite-backed `IntervalAnnotationStore`.

* **Parameters:**
  * **path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike)) – Path to the `.annot` file. Use `":memory:"` for an
    ephemeral in-memory database.
  * **check_same_thread** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Forwarded to `sqlite3.connect`. We hold a
    single connection guarded by a lock; pass `False` when
    sharing across threads.
  * **migrate** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Opt-in to upgrading a file written at an older
    `schema_version` on open, via the ladder in
    [`lacing.store.migrations`](_autosummary/lacing.store.migrations.html.md#module-lacing.store.migrations). Off by default — silently
    rewriting someone’s file on open is worse than refusing.
    Every open-time schema failure — refusal *or* failed migration
    — raises [`SchemaMismatchError`](_autosummary/lacing.html.md#lacing.SchemaMismatchError); a failed migration
    chains the ladder’s `StoreMigrationError` as its cause.

#### lock(key)

Exclusive lock named `key`: a file lock beside the database.

Excludes other threads and other processes on this host that open the
same `.annot` file; the kernel frees it if the holder dies. An
in-memory database (`":memory:"`) has no file, so it falls back to a
process-local lock.

* **Return type:**
  [`AbstractContextManager`](https://docs.python.org/3/library/contextlib.html#contextlib.AbstractContextManager)[[`None`](https://docs.python.org/3/builtins/constants.html#None)]

#### update(annotation)

Replace the row with this id (and its R\*Tree entry) in one transaction.

Readers take the same lock (`_fetchall()`), so on a connection
shared between threads — where a reader would otherwise see this
transaction’s uncommitted state — they get the old annotation or the
new one, never neither (lacing#66).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *exception* lacing.StoreMigrationError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised when a store migration step is missing or fails.

### *class* lacing.Tier(name, , stereotype=TierStereotype.NONE, parent=None, metadata=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A named annotation layer with optional parent and stereotype.

Tiers are pure metadata; they don’t own annotations. The store is keyed
by interval, not by tier — annotations carry their tier name as a field.
This matches ELAN’s TIME_ORDER indirection (see ANN-DOC §C).

### *class* lacing.TierStereotype(\*values)

Bases: [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Enum`](https://docs.python.org/3/library/enum.html#enum.Enum)

Constraints on how a child tier relates to its parent tier.

Names match ELAN exactly so EAF round-trips are trivial.

#### INCLUDED_IN *= 'INCLUDED_IN'*

Children lie within the parent but gaps between siblings are allowed.

#### NONE *= 'NONE'*

No parent constraint. Top-level tier.

#### SYMBOLIC_ASSOCIATION *= 'SYMBOLIC_ASSOCIATION'*

One-to-one association with parent; child shares parent’s interval exactly.

#### SYMBOLIC_SUBDIVISION *= 'SYMBOLIC_SUBDIVISION'*

Ordered subdivision; children share parent’s interval as a sequence (no times).

#### TIME_SUBDIVISION *= 'TIME_SUBDIVISION'*

Children fully partition the parent’s interval (no gaps, no overlap).

### *class* lacing.TimeInterval(start, end)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A half-open interval `[start, end)`.

`start == end` is a valid point annotation, not a degenerate case.
Always `start <= end`; constructor raises `ValueError` otherwise.

#### *property* duration *: [RationalTime](_autosummary/lacing.time.html.md#lacing.time.RationalTime)*

`end - start` at the same rate as `start`.

#### *classmethod* from_wire(d)

Build from the wire form. See [`RationalTime.from_wire()`](_autosummary/lacing.html.md#lacing.RationalTime.from_wire) on extras.

* **Return type:**
  [`TimeInterval`](_autosummary/lacing.time.html.md#lacing.time.TimeInterval)

### *exception* lacing.UnknownBodySchemaError

Bases: [`KeyError`](https://docs.python.org/3/builtins/exceptions.html#KeyError)

Raised when an annotation’s body_schema_uri has no registered model.

### lacing.annotation_body_digest(annotation)

Return the SHA-256 hex digest of `{body, body_schema_uri}` only.

The narrow sibling of [`annotation_value_digest()`](_autosummary/lacing.html.md#lacing.annotation_value_digest). It drops the
**entire** `reference` — *which asset* / *which node* / \*which
annotation\*, not merely *when* — plus `tier` and `confidence`. So the
same caption over two **different assets** digests identically here, as
does the same body asserted on two different tiers or at two different
confidences.

That is a correctness bug in any consumer that reads any of those. Reach
for it only when the consumer demonstrably depends on nothing but what the
annotation *says*; prefer [`annotation_value_digest()`](_autosummary/lacing.html.md#lacing.annotation_value_digest) otherwise.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> from uuid import uuid4
>>> from lacing import Annotation, MediaRef, Provenance
>>> from lacing import RationalTime, TimeInterval
>>> def over(asset):
...     return Annotation(
...         id=uuid4(), tier="words",
...         reference=MediaRef(
...             asset_id=asset,
...             interval=TimeInterval(RationalTime(0), RationalTime(24000)),
...         ),
...         body={"text": "hello"},
...         body_schema_uri="annot://schema/word/v1",
...         provenance=Provenance(
...             was_generated_by="agent:m@1",
...             was_attributed_to="thor",
...             generated_at_time=RationalTime.now(),
...         ),
...     )
```

Different **assets**, identical body digest — this is the footgun:

```pycon
>>> annotation_body_digest(over("sha256:interview")) == (
...     annotation_body_digest(over("sha256:broadcast"))
... )
True
>>> annotation_value_digest(over("sha256:interview")) == (
...     annotation_value_digest(over("sha256:broadcast"))
... )
False
```

```pycon
>>> from uuid import uuid4
>>> from lacing import Annotation, MediaRef, Provenance
>>> from lacing import RationalTime, TimeInterval
>>> def make(interval):
...     return Annotation(
...         id=uuid4(),
...         tier="words",
...         reference=MediaRef(asset_id="sha256:abc", interval=interval),
...         body={"text": "hello"},
...         body_schema_uri="annot://schema/word/v1",
...         provenance=Provenance(
...             was_generated_by="agent:m@1",
...             was_attributed_to="thor",
...             generated_at_time=RationalTime.now(),
...         ),
...     )
>>> early = make(TimeInterval(RationalTime(0), RationalTime(24000)))
>>> late = make(TimeInterval(RationalTime(24000), RationalTime(48000)))
>>> annotation_body_digest(early) == annotation_body_digest(late)
True
>>> annotation_value_digest(early) == annotation_value_digest(late)
False
```

### lacing.annotation_value_digest(annotation)

Return the SHA-256 hex digest of `annotation`’s value.

Covers `body`, `body_schema_uri`, `tier`, `reference` and
`confidence`. Excludes `id` and `provenance`, so a regeneration that
produces identical content produces an identical digest.

Use this for freshness and early cutoff. For optimistic concurrency use
`lacing.server.etag.annotation_etag()` instead — two digests, two
jobs, and neither substitutes for the other.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> from uuid import uuid4
>>> from lacing import Annotation, MediaRef, Provenance
>>> from lacing import RationalTime, TimeInterval
>>> def make(**kw):
...     base = dict(
...         id=uuid4(),
...         tier="words",
...         reference=MediaRef(
...             asset_id="sha256:abc",
...             interval=TimeInterval(RationalTime(0), RationalTime(24000)),
...         ),
...         body={"text": "hello"},
...         body_schema_uri="annot://schema/word/v1",
...         provenance=Provenance(
...             was_generated_by="agent:m@1",
...             was_attributed_to="thor",
...             generated_at_time=RationalTime.now(),
...         ),
...     )
...     base.update(kw)
...     return Annotation(**base)
```

A regeneration — new `id`, new timestamp, same content — digests the same:

```pycon
>>> a = make()
>>> b = make(provenance=Provenance(
...     was_generated_by="agent:m@1",
...     was_attributed_to="thor",
...     generated_at_time=RationalTime(999),
... ))
>>> annotation_value_digest(a) == annotation_value_digest(b)
True
```

A changed body does not:

```pycon
>>> annotation_value_digest(make(body={"text": "goodbye"})) == (
...     annotation_value_digest(a)
... )
False
```

### lacing.boundary_iou(a, b)

Mean IoU between two sets of intervals via greedy best-match.

For each interval in `a`, finds its highest-IoU match in `b` (without
replacement — once a `b` interval is matched it’s removed from the pool).
Unmatched intervals in either set contribute 0.0 to the mean.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)
* **Returns:**
  Mean IoU ∈ [0, 1]. Returns 0.0 if both sets are empty (defensible
  as a “no agreement to measure” baseline).

### lacing.cohen_kappa(a, b)

Cohen’s kappa for two annotators on a categorical label.

* **Parameters:**
  * **a** ([`Sequence`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`, bound= [`Hashable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Hashable))]) – Annotator A’s labels.
  * **b** ([`Sequence`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`, bound= [`Hashable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Hashable))]) – Annotator B’s labels (must be the same length).
* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)
* **Returns:**
  κ ∈ [-1, 1]. 1 = perfect agreement, 0 = chance, negative = worse than chance.
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If sequences differ in length or are empty.

Edge cases:
: If only one category appears across both annotators, both observed
  and expected agreement are 1.0; we return 1.0 by convention.

### lacing.export_json_schemas(target_dir, , overwrite=True, include_meta=True, allow_empty=False)

Write every registered schema as JSON files under `target_dir`.

Layout: `<target_dir>/<name>/v<N>.json`. Returns the list of paths
written, in registration order.

* **Parameters:**
  * **target_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Output directory. Created if missing.
  * **overwrite** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If False, refuse to write a file that already exists.
  * **include_meta** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, also write a `<target_dir>/index.json`
    mapping every URI to its file path and the Pydantic model’s
    qualified name (helps the codegen pipeline).
  * **allow_empty** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, permit an export from an empty registry. The
    default refuses, because the overwhelmingly likely cause is a
    missing `import lacing.bodies` and the write would destroy the
    committed artifacts.
* **Raises:**
  [**EmptySchemaRegistryError**](_autosummary/lacing.html.md#lacing.EmptySchemaRegistryError) – Nothing is registered and `allow_empty`
      is False.
* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]

### lacing.get_tracer(name='lacing', version=None)

Return a tracer or a no-op fallback.

* **Parameters:**
  * **name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Instrumentation name (typically `__name__` of the caller’s
    module or a logical name like `"lacing.server"`).
  * **version** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Optional package version string.
* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)
* **Returns:**
  `opentelemetry.trace.Tracer` if OTel is installed, else a no-op
  object whose `start_as_current_span()` is a context manager
  yielding a no-op span.

### lacing.hash_bytes(data)

Return the canonical `asset_id` (SHA-256 hex) for `data`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> hash_bytes(b"hello world")[:8]
'b94d27b9'
```

### lacing.hash_file(path, , chunk_size=1048576)

Return the canonical `asset_id` (SHA-256 hex) for the file at `path`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### lacing.instrument_otel(app, , tracer_name='lacing.server')

Add OpenTelemetry instrumentation to a FastAPI app.

Wraps every request in a span; tags the span with the response’s
`X-Lacing-Clock` header value (when present) as `lacing.clock`.

No-op when OTel isn’t installed — the app is returned unchanged.

* **Parameters:**
  * **app** ([`Any`](https://docs.python.org/3/library/typing.html#typing.Any)) – The FastAPI app (from `lacing.server.create_app()`).
  * **tracer_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name passed to [`get_tracer()`](_autosummary/lacing.html.md#lacing.get_tracer).
* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)
* **Returns:**
  The same app, with middleware installed if OTel is available.

### lacing.interval_iou(a, b)

Intersection-over-Union for two time intervals.

Returns 1.0 if both are equal point intervals at the same instant; 0.0
if they don’t intersect (including when only one is a point).

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

### lacing.is_otel_active()

Quick check: is OTel installed AND a TracerProvider configured?

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### lacing.json_schema(uri)

Return the JSON Schema for the body model registered at `uri`.

Pydantic’s `model_json_schema()` output, plus a `default` for every
zero-argument `default_factory` field (see
`lacing.json_schema.DefaultFactoryJsonSchema`), whatever base class
the registered model has.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### lacing.krippendorff_alpha(annotations, \*, distance=<function \_nominal_distance>)

Krippendorff’s α across any number of annotators.

* **Parameters:**
  * **annotations** ([`Sequence`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence)[[`Sequence`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence)[[`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`, bound= [`Hashable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Hashable))]]]) – A list of annotators, each a sequence of labels (one per
    unit). Use `None` for a missing annotation by that annotator on
    that unit.
  * **distance** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`, bound= [`Hashable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Hashable)), [`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`, bound= [`Hashable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Hashable))], [`float`](https://docs.python.org/3/builtins/functions.html#float)]) – Function `(x, y) -> float` measuring disagreement between
    two label values. Default is the nominal (0/1) distance.
* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)
* **Returns:**
  α. 1.0 = perfect agreement, 0.0 = chance.
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If sequences differ in length, fewer than 2 annotators
      given, or fewer than 2 paired observations exist.

### lacing.maybe_span(tracer, name, \*\*attributes)

Open a span on `tracer`, attaching `attributes` if supported.

Works with both real OTel tracers and the no-op fallback.

### lacing.migrate(body, , from_uri, to_uri)

Migrate `body` from `from_uri` to `to_uri` via registered steps.

Composes single-step migrations. Raises [`MigrationError`](_autosummary/lacing.html.md#lacing.MigrationError) if any
step is missing.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### lacing.migrate_annot_file(path, , to_version=None)

Migrate a `.annot` file in place, returning `(from, to)` versions.

`to_version` defaults to the current build’s
[`lacing.store.sqlite.SCHEMA_VERSION`](_autosummary/lacing.store.sqlite.html.md#lacing.store.sqlite.SCHEMA_VERSION). Already-current files are a
no-op (`from == to`). Each step runs in its own `BEGIN IMMEDIATE`
transaction with the version re-checked under the lock, so concurrent
migrators converge and an interrupted chain resumes from the last
version that completed (idempotent).

Raises [`StoreMigrationError`](_autosummary/lacing.html.md#lacing.StoreMigrationError) when the file does not exist, is
not a `.annot` file, a step is missing, fails, or breaks one of the
runner’s in-transaction guarantees.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`int`](https://docs.python.org/3/builtins/functions.html#int)]

### lacing.register_body_schema(uri, model)

Register `model` as the validator for `uri`. Returns `model`.

* **Return type:**
  [`type`](https://docs.python.org/3/builtins/functions.html#type)[`BaseModel`]

### lacing.register_migration(, schema_name, from_version, to_version)

Register a forward migration from `v<from_version>` to `v<to_version>`.

Decorated function takes a body `dict` and returns a new body `dict`.
Migrations must be one major-version step at a time
(`to_version == from_version + 1`).

Re-registering the same `(schema_name, from_version)` pair replaces
the previous entry — convenient in tests, intentional for hot-reload.

### lacing.register_processor(func=None, , name=None)

Register a processor under `name` (defaults to the function name).

The function may be sync or async; we wrap sync funcs to a coroutine.

### lacing.register_store_migration(, store_kind, from_version, to_version)

Register a forward store migration from `from_version` to `to_version`.

The decorated function takes the backend’s open connection and must
perform every change of the step — DDL, row rewrites, and the
`meta.schema_version` write. Steps must be one version at a time
(`to_version == from_version + 1`); the runner chains them.

Step authors: read the module docstring’s rules — no `executescript`
/ `COMMIT` / `ROLLBACK` inside a step, preserve rowids on table
rebuilds, and rebuild the interval index with
`rebuild_annotations_rtree()` if the `annotations` table was
rebuilt.

Re-registering the same `(store_kind, from_version)` pair replaces the
previous entry.

### lacing.registered_processors()

Names of every registered processor, sorted.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### lacing.render_artifact_exhibit(annotations, , out_dir, formats=('html', 'pdf', 'md'), title='Artifact exhibit', image_resolver=None)

Render an annotation graph as a human-readable artifact exhibit.

Lays every annotation out as a card — body, panel images, and
in-document hyperlinks to the artifacts it derives from / feeds
into. **HTML is authored**; the PDF and Markdown derive from it.

Panel images are written once as content-addressed sibling files
under `<out_dir>/images/` and referenced relatively, so the HTML
and Markdown stay small; the PDF embeds them and stays a single
self-contained file. The `images/` directory is created only when
the graph actually has images.

* **Parameters:**
  * **annotations** ([`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)) – the lacing annotations to exhibit (any iterable).
    Order is preserved — pass them chain-ordered for a document
    that reads top-to-bottom.
  * **out_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – directory the `exhibit.{html,pdf,md}` files land in.
  * **formats** ([`Sequence`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – which of `html` / `pdf` / `md` to write. The HTML
    is always built in memory (the others derive from it).
  * **title** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – document title.
  * **image_resolver** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)], [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]]) – `image-reference → local file path` callback.
    Defaults to reading the reference’s own `path` field; a
    caller whose images live behind URLs passes a resolver that
    downloads / caches them (keeping this module media-agnostic).
* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]
* **Returns:**
  The written file paths.
* **Raises:**
  [**RuntimeError**](https://docs.python.org/3/builtins/exceptions.html#RuntimeError) – when `pdf` / `md` is requested but its optional
      converter (`weasyprint` / `dn`) is not installed — the
      message names the install command.

### lacing.replay_oplog(log, , until_clock=None, target_factory=None)

Rebuild a store by replaying `log` up to (and including) `until_clock`.

* **Parameters:**
  * **log** ([`OpLog`](_autosummary/lacing.oplog.html.md#lacing.oplog.OpLog)) – Source op-log.
  * **until_clock** ([`int`](https://docs.python.org/3/builtins/functions.html#int) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Stop at this clock (inclusive). None = replay all.
  * **target_factory** – Zero-arg callable returning a fresh empty store.
    Defaults to `MemoryStore`. Pass a `SqliteStore` factory to
    replay into a persistent file.
* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)
* **Returns:**
  The rebuilt store. Operations whose payload references unknown
  body schemas or tier parents are still applied; the caller is
  responsible for any post-replay validation.

### *async* lacing.run_processor_async(name, , store, oplog, \*\*kwargs)

Run a processor in the current event loop. For async callers.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### lacing.run_processor_sync(name, , store, oplog, \*\*kwargs)

Run a processor synchronously and return its result.

If the processor is async, we call it via `asyncio.run` (when no
loop is running) or schedule and wait on it (when a loop is already
active). Most callers from sync code want the former.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### lacing.traced(tracer, span_name=None, , record_args=False)

Decorator: wrap a function in a span on `tracer`.

* **Parameters:**
  * **tracer** ([`Any`](https://docs.python.org/3/library/typing.html#typing.Any)) – From [`get_tracer()`](_autosummary/lacing.html.md#lacing.get_tracer).
  * **span_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Override the span name. Default: `func.__qualname__`.
  * **record_args** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, attach (str-coerced) positional + keyword
    args as span attributes `arg.<n>` / `kwarg.<name>`. Off
    by default — args may contain large or sensitive data.

### lacing.validate_body(body, uri)

Validate `body` against the schema registered for `uri`.

Returns the parsed Pydantic instance. Raises [`BodySchemaError`](_autosummary/lacing.html.md#lacing.BodySchemaError)
on validation failure (wrapping the underlying `pydantic.ValidationError`)
or [`UnknownBodySchemaError`](_autosummary/lacing.html.md#lacing.UnknownBodySchemaError) if the URI isn’t registered.

* **Return type:**
  `BaseModel`

### Modules

| [`adapters`](_autosummary/lacing.adapters.html.md#module-lacing.adapters)             | I/O adapter registry.                                                     |
|----------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|
| [`allen`](_autosummary/lacing.allen.html.md#module-lacing.allen)                   | Allen's 13 interval relations as pure predicates.                         |
| [`artifact`](_autosummary/lacing.artifact.html.md#module-lacing.artifact)             | Content-addressed artifact references.                                    |
| [`artifact_store`](_autosummary/lacing.artifact_store.html.md#module-lacing.artifact_store) | Artifact store — a metadata catalog and a blob store behind one facade.   |
| [`bodies`](_autosummary/lacing.bodies.html.md#module-lacing.bodies)                 | Built-in body schemas.                                                    |
| [`cli`](_autosummary/lacing.cli.html.md#module-lacing.cli)                       | Command-line interface for lacing.                                        |
| [`digest`](_autosummary/lacing.digest.html.md#module-lacing.digest)                 | Content digests over an annotation's *value* — the freshness primitive.   |
| [`exhibit`](_autosummary/lacing.exhibit.html.md#module-lacing.exhibit)               | Artifact exhibit — render an annotation graph as a readable document.     |
| [`model`](_autosummary/lacing.model.html.md#module-lacing.model)                   | Annotation envelope, references, and provenance.                          |
| [`oplog`](_autosummary/lacing.oplog.html.md#module-lacing.oplog)                   | Operation log for time-travel debug + audit.                              |
| [`otel`](_autosummary/lacing.otel.html.md#module-lacing.otel)                     | Optional OpenTelemetry instrumentation for the lacing server.             |
| [`processors`](_autosummary/lacing.processors.html.md#module-lacing.processors)         | Background processors — pluggable jobs that run against a store + op-log. |
| [`quality`](_autosummary/lacing.quality.html.md#module-lacing.quality)               | Inter-annotator agreement and boundary metrics.                           |
| [`schema`](_autosummary/lacing.schema.html.md#module-lacing.schema)                 | Body schema registry, JSON Schema export, and migrations.                 |
| [`store`](_autosummary/lacing.store.html.md#module-lacing.store)                   | Interval-keyed annotation stores.                                         |
| [`tier`](_autosummary/lacing.tier.html.md#module-lacing.tier)                     | Tiers and the five ELAN stereotypes.                                      |
| [`time`](_autosummary/lacing.time.html.md#module-lacing.time)                     | Rational time and half-open intervals — the foundations.                  |
| [`tracks`](_autosummary/lacing.tracks.html.md#module-lacing.tracks)                 | High-level "track-shaped" facades on top of `lacing`.                     |
| [`worker`](_autosummary/lacing.worker.html.md#module-lacing.worker)                 | Optional Arq integration — queue processors through Redis.                |


# _autosummary/lacing.model.html.md

# lacing.model

Annotation envelope, references, and provenance.

One envelope, typed body. The `body: dict` is validated by the schema at
`body_schema_uri` (semver). No polymorphic class hierarchy. See BACK-DOC §2.1
and `.claude/skills/lacing-schema-codegen/SKILL.md`.

### Module Attributes

| [`Reference`](_autosummary/lacing.model.html.md#lacing.model.Reference)            | Discriminated union on `kind`.                                                                                                                                                                   |
|-----------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`AssetId`](_autosummary/lacing.model.html.md#lacing.model.AssetId)              | A content-addressed artifact identity — the SHA-256 of the artifact's bytes, as [`lacing.Artifact`](_autosummary/lacing.html.md#lacing.Artifact) enforces: **bare 64-hex only**. |
| [`ProvenanceRef`](_autosummary/lacing.model.html.md#lacing.model.ProvenanceRef)        | an annotation `id` (UUID) or an artifact `asset_id` (64-hex SHA-256).                                                                                                                            |
| [`UNKNOWN_GENERATED_AT`](_autosummary/lacing.model.html.md#lacing.model.UNKNOWN_GENERATED_AT) | The `generated_at_time` an annotation carries when its generation time is **unknown** — tick 0, at any rate.                                                                                     |

### Functions

| [`partition_provenance_refs`](_autosummary/lacing.model.html.md#lacing.model.partition_provenance_refs)(refs)   | Split `was_derived_from` into `(annotation_ids, asset_ids)`.   |
|------------------------------------------------------------------------------------|----------------------------------------------------------------|

### Classes

| [`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)(\*\*data)    | The single annotation envelope.                                                |
|--------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| [`AnnotationRef`](_autosummary/lacing.model.html.md#lacing.model.AnnotationRef)(\*\*data) | Reference to another annotation (for discussion threads, review, derivations). |
| [`MediaRef`](_autosummary/lacing.model.html.md#lacing.model.MediaRef)(\*\*data)      | Reference to a region of a content-addressed media asset.                      |
| [`NodeRef`](_autosummary/lacing.model.html.md#lacing.model.NodeRef)(\*\*data)       | Reference to a node in a structured scene/document graph.                      |
| [`Provenance`](_autosummary/lacing.model.html.md#lacing.model.Provenance)(\*\*data)    | W3C PROV-O subset, embedded inline on every annotation.                        |

### *class* lacing.model.Annotation(\*\*data)

Bases: `SchemaModel`

The single annotation envelope. `body` is typed by `body_schema_uri`.

#### *property* interval *: [TimeInterval](_autosummary/lacing.time.html.md#lacing.time.TimeInterval) | [None](https://docs.python.org/3/builtins/constants.html#None)*

the reference’s interval, if any.

* **Type:**
  Convenience

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* lacing.model.AnnotationRef(\*\*data)

Bases: `SchemaModel`

Reference to another annotation (for discussion threads, review, derivations).

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### lacing.model.AssetId

A content-addressed artifact identity — the SHA-256 of the artifact’s
bytes, as [`lacing.Artifact`](_autosummary/lacing.html.md#lacing.Artifact) enforces: **bare 64-hex only**.
`MediaRef.asset_id`-style prefixed identifiers (`blake3:…`,
`sha256:…`) are a different, free-form vocabulary and are NOT
provenance refs — copying one in here is a loud `ValidationError`, by
design. Format-disjoint from a UUID string (36 hyphenated chars), so the
[`ProvenanceRef`](_autosummary/lacing.model.html.md#lacing.model.ProvenanceRef) union is unambiguous.

alias of `Annotated`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), FieldInfo(annotation=NoneType, required=True, metadata=[MinLen(min_length=64), MaxLen(max_length=64), \_PydanticGeneralMetadata(pattern=’^[0-9a-f]{64}$’)])]

### *class* lacing.model.MediaRef(\*\*data)

Bases: `SchemaModel`

Reference to a region of a content-addressed media asset.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* lacing.model.NodeRef(\*\*data)

Bases: `SchemaModel`

Reference to a node in a structured scene/document graph.

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### *class* lacing.model.Provenance(\*\*data)

Bases: `SchemaModel`

W3C PROV-O subset, embedded inline on every annotation.

#### *property* generated_at_is_known *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

`False` when `generated_at_time` is the [`UNKNOWN_GENERATED_AT`](_autosummary/lacing.model.html.md#lacing.model.UNKNOWN_GENERATED_AT) sentinel.

The one test every freshness or ordering consumer should make before
comparing `generated_at_time` values: an unknown time cannot be
ordered against a known one, and a consumer that compares anyway
reads the row as older than everything (lacing#44).

```pycon
>>> from lacing.time import RationalTime
>>> kw = dict(was_generated_by="user:x", was_attributed_to="x")
>>> Provenance(generated_at_time=RationalTime(0, 1000), **kw).generated_at_is_known
False
>>> Provenance(generated_at_time=RationalTime.now(), **kw).generated_at_is_known
True
```

#### model_config *: [ClassVar](https://docs.python.org/3/library/typing.html#typing.ClassVar)[ConfigDict]* *= {'extra': 'forbid', 'frozen': True}*

Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].

### lacing.model.ProvenanceRef

an annotation `id`
(UUID) or an artifact `asset_id` (64-hex SHA-256). Widened from bare
`list[UUID]` by lacing#14 (defect D5) so artifact-to-artifact lineage —
the tier where the expensive things live — is representable at all.
Consumers that walk lineage discriminate with
[`partition_provenance_refs()`](_autosummary/lacing.model.html.md#lacing.model.partition_provenance_refs). Derivation *roles* (lacing#17) will
arrive as a separate additive qualification field, PROV-O-style — this
list stays the simple edge walk.

* **Type:**
  One upstream reference in `was_derived_from`

alias of [`UUID`](https://docs.python.org/3/library/uuid.html#uuid.UUID) | `Annotated`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), FieldInfo(annotation=NoneType, required=True, metadata=[MinLen(min_length=64), MaxLen(max_length=64), \_PydanticGeneralMetadata(pattern=’^[0-9a-f]{64}$’)])]

### lacing.model.Reference

Discriminated union on `kind`.

alias of `Annotated`[[`MediaRef`](_autosummary/lacing.model.html.md#lacing.model.MediaRef) | [`NodeRef`](_autosummary/lacing.model.html.md#lacing.model.NodeRef) | [`AnnotationRef`](_autosummary/lacing.model.html.md#lacing.model.AnnotationRef), FieldInfo(annotation=NoneType, required=True, discriminator=’kind’)]

### lacing.model.UNKNOWN_GENERATED_AT *: [RationalTime](_autosummary/lacing.time.html.md#lacing.time.RationalTime)* *= RationalTime(0, 24000)*

The `generated_at_time` an annotation carries when its generation time is
**unknown** — tick 0, at any rate.

This is a sentinel, never a timestamp. `RationalTime` is wall-clock time
in the provenance role (`RationalTime.now()`), and tick 0 is *not* “the
epoch, 1970-01-01”: no annotation was generated then. It is what a writer
stamps when it has nothing better (the REST path did so until lacing#35),
and what every row written that way still carries, since lacing does no
backfill (lacing#44). `RationalTime(0, r) == RationalTime(0, s)` for any
rates, so comparing against this constant is rate-independent.

Consumers must read it as *unknown*, and unknown resolves the safe way:
an annotation whose own generation time — or whose upstream parent’s — is
unknown is **unverifiable**, hence stale, never “the oldest thing in the
project”. Test with [`Provenance.generated_at_is_known`](_autosummary/lacing.model.html.md#lacing.model.Provenance.generated_at_is_known); never order
a tick-0 annotation against a real timestamp.

### lacing.model.partition_provenance_refs(refs)

Split `was_derived_from` into `(annotation_ids, asset_ids)`.

The one discriminator every lineage walker needs, provided centrally so
no consumer re-derives (or half-derives) the union rule.

`refs` must come from a **validated** [`Provenance`](_autosummary/lacing.model.html.md#lacing.model.Provenance) — the split
is by runtime type (`UUID` vs `str`), so a raw wire list that never
passed validation would land every UUID *string* in the asset bucket.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`UUID`](https://docs.python.org/3/library/uuid.html#uuid.UUID)], [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]


# _autosummary/lacing.oplog.html.md

# lacing.oplog

Operation log for time-travel debug + audit.

The op-log is an append-only sequence of mutations applied to a store.
Each entry carries a monotonically increasing **Lamport clock**, the
operation name (`add_annotation`, `remove_annotation`, `add_tier`,
…), the target id, a JSON payload sufficient to **replay** the
operation against a fresh store, plus actor / timestamp metadata.

Together with a fresh empty store, the op-log is enough to reconstruct
the state of the system at any past clock value — the “killer debug
feature” called out in BACK-DOC §4.7.

Two implementations ship with Phase 2:

- [`InMemoryOpLog`](_autosummary/lacing.oplog.html.md#lacing.oplog.InMemoryOpLog) — for tests, dev, ephemeral runs.
- [`SqliteOpLog`](_autosummary/lacing.oplog.html.md#lacing.oplog.SqliteOpLog) — persistent table on top of any SQLite database
  (intended to live in the same `.annot` file as the store).

Use [`replay()`](_autosummary/lacing.oplog.html.md#lacing.oplog.replay) to rebuild a store at a given clock.

### Functions

| `has_entries`(log)                                                                              |                                                                         |
|-------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`replay`](_autosummary/lacing.oplog.html.md#lacing.oplog.replay)(log, \*[, until_clock, target_factory]) | Rebuild a store by replaying `log` up to (and including) `until_clock`. |

### Classes

| [`InMemoryOpLog`](_autosummary/lacing.oplog.html.md#lacing.oplog.InMemoryOpLog)()                                  | Simple list-backed op-log.       |
|---------------------------------------------------------------------------------------------------|----------------------------------|
| [`OpLog`](_autosummary/lacing.oplog.html.md#lacing.oplog.OpLog)(\*args, \*\*kwargs)                        | Append-only log of mutations.    |
| [`OpLogEntry`](_autosummary/lacing.oplog.html.md#lacing.oplog.OpLogEntry)(clock, operation, target_id, payload) | One row of the op-log.           |
| [`SqliteOpLog`](_autosummary/lacing.oplog.html.md#lacing.oplog.SqliteOpLog)(path, \*[, check_same_thread])       | Op-log backed by a SQLite table. |

### *class* lacing.oplog.InMemoryOpLog

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Simple list-backed op-log. Thread-safe via an RLock.

### *class* lacing.oplog.OpLog(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

Append-only log of mutations.

Implementations must guarantee that the clock returned by
[`append()`](_autosummary/lacing.oplog.html.md#lacing.oplog.OpLog.append) is strictly greater than every previously-returned
clock value across the lifetime of the log.

#### append(operation, , target_id=None, payload=None, actor='anonymous')

Append an entry; return its assigned clock.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

#### entries(, until_clock=None, from_clock=None)

Iterate entries, optionally bounded by clock range (inclusive).

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`OpLogEntry`](_autosummary/lacing.oplog.html.md#lacing.oplog.OpLogEntry)]

#### latest_clock()

Highest clock currently in the log; 0 if empty.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

### *class* lacing.oplog.OpLogEntry(clock, operation, target_id, payload, actor='anonymous', received_at=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One row of the op-log.

#### actor *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

`user:<handle>` or `agent:<model>@<hash>` or `adapter:<format>`.

#### clock *: [int](https://docs.python.org/3/builtins/functions.html#int)*

Monotonic Lamport clock starting at 1. Strictly increasing per log.

#### operation *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

`add_annotation`, `remove_annotation`, `update_annotation`,
`add_tier`, `set_meta`, `import_batch`.

* **Type:**
  One of

#### payload *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]*

JSON-serializable payload sufficient to replay the operation.

#### received_at *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Wall-clock time the operation was received (seconds since epoch).

#### target_id *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Annotation id, tier name, meta key, or None for batch ops.

### *class* lacing.oplog.SqliteOpLog(path, , check_same_thread=True)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Op-log backed by a SQLite table.

Designed to share a database file with `SqliteStore` so the store
snapshot + the op-log live together and survive a single
`cp project.annot project.backup` step.

### lacing.oplog.replay(log, , until_clock=None, target_factory=None)

Rebuild a store by replaying `log` up to (and including) `until_clock`.

* **Parameters:**
  * **log** ([`OpLog`](_autosummary/lacing.oplog.html.md#lacing.oplog.OpLog)) – Source op-log.
  * **until_clock** ([`int`](https://docs.python.org/3/builtins/functions.html#int) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Stop at this clock (inclusive). None = replay all.
  * **target_factory** – Zero-arg callable returning a fresh empty store.
    Defaults to `MemoryStore`. Pass a `SqliteStore` factory to
    replay into a persistent file.
* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)
* **Returns:**
  The rebuilt store. Operations whose payload references unknown
  body schemas or tier parents are still applied; the caller is
  responsible for any post-replay validation.


# _autosummary/lacing.otel.html.md

# lacing.otel

Optional OpenTelemetry instrumentation for the lacing server.

OTel is the “cherry on top” of the op-log architecture (BACK-DOC §4.7).
The op-log captures *what* happened; OTel publishes spans so downstream
distributed-tracing infra (Jaeger, Tempo, Honeycomb, …) can correlate
those mutations with calls from upstream services.

Design rules:

- **Optional.** `opentelemetry-*` packages are heavy. This module
  imports cleanly without them — every helper falls back to a no-op
  when OTel isn’t installed.
- **Auto-detect.** If `opentelemetry.api` is importable AND a
  `TracerProvider` has been configured (via the standard env vars
  `OTEL_EXPORTER_*` or programmatic setup), spans are emitted.
  Otherwise everything is a no-op and there’s no perf cost.
- **No globals.** Callers pass tracers around explicitly.

Usage:

```default
from lacing.otel import get_tracer, traced

tracer = get_tracer("lacing.server")

@traced(tracer, "my_operation", record_args=True)
def my_op(x, y):
    return x + y
```

In Phase 2.0+, the FastAPI middleware (`lacing.server.otel_middleware`,
opt-in) wraps every request in a span and tags it with the Lamport clock
returned by the op-log. The ASGI middleware is added to the app via
[`instrument_app()`](_autosummary/lacing.otel.html.md#lacing.otel.instrument_app) only when explicitly called — we don’t auto-mount
it because that would change behavior depending on whether opentelemetry
happened to be installed.

### Functions

| [`get_tracer`](_autosummary/lacing.otel.html.md#lacing.otel.get_tracer)([name, version])              | Return a tracer or a no-op fallback.                            |
|-------------------------------------------------------------------------------------------|-----------------------------------------------------------------|
| [`instrument_app`](_autosummary/lacing.otel.html.md#lacing.otel.instrument_app)(app, \*[, tracer_name])   | Add OpenTelemetry instrumentation to a FastAPI app.             |
| [`is_otel_active`](_autosummary/lacing.otel.html.md#lacing.otel.is_otel_active)()                         | Quick check: is OTel installed AND a TracerProvider configured? |
| [`maybe_span`](_autosummary/lacing.otel.html.md#lacing.otel.maybe_span)(tracer, name, \*\*attributes) | Open a span on `tracer`, attaching `attributes` if supported.   |
| [`traced`](_autosummary/lacing.otel.html.md#lacing.otel.traced)(tracer[, span_name, record_args]) | Decorator: wrap a function in a span on `tracer`.               |

### lacing.otel.get_tracer(name='lacing', version=None)

Return a tracer or a no-op fallback.

* **Parameters:**
  * **name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Instrumentation name (typically `__name__` of the caller’s
    module or a logical name like `"lacing.server"`).
  * **version** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Optional package version string.
* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)
* **Returns:**
  `opentelemetry.trace.Tracer` if OTel is installed, else a no-op
  object whose `start_as_current_span()` is a context manager
  yielding a no-op span.

### lacing.otel.instrument_app(app, , tracer_name='lacing.server')

Add OpenTelemetry instrumentation to a FastAPI app.

Wraps every request in a span; tags the span with the response’s
`X-Lacing-Clock` header value (when present) as `lacing.clock`.

No-op when OTel isn’t installed — the app is returned unchanged.

* **Parameters:**
  * **app** ([`Any`](https://docs.python.org/3/library/typing.html#typing.Any)) – The FastAPI app (from `lacing.server.create_app()`).
  * **tracer_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name passed to [`get_tracer()`](_autosummary/lacing.otel.html.md#lacing.otel.get_tracer).
* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)
* **Returns:**
  The same app, with middleware installed if OTel is available.

### lacing.otel.is_otel_active()

Quick check: is OTel installed AND a TracerProvider configured?

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### lacing.otel.maybe_span(tracer, name, \*\*attributes)

Open a span on `tracer`, attaching `attributes` if supported.

Works with both real OTel tracers and the no-op fallback.

### lacing.otel.traced(tracer, span_name=None, , record_args=False)

Decorator: wrap a function in a span on `tracer`.

* **Parameters:**
  * **tracer** ([`Any`](https://docs.python.org/3/library/typing.html#typing.Any)) – From [`get_tracer()`](_autosummary/lacing.otel.html.md#lacing.otel.get_tracer).
  * **span_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Override the span name. Default: `func.__qualname__`.
  * **record_args** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, attach (str-coerced) positional + keyword
    args as span attributes `arg.<n>` / `kwarg.<name>`. Off
    by default — args may contain large or sensitive data.


# _autosummary/lacing.processors.html.md

# lacing.processors

Background processors — pluggable jobs that run against a store + op-log.

A *processor* is a registered async function that:

1. Takes an `IntervalAnnotationStore`, an `OpLog`, and processor-specific
   keyword arguments.
2. Either mutates the store (recording op-log entries) or returns a result
   (or both).

Two execution modes ship:

- **Synchronous** via [`run_sync()`](_autosummary/lacing.processors.html.md#lacing.processors.run_sync) — runs in the current event loop.
  Always available; no Redis or other infra. Use this from tests, CLIs,
  and scripts.
- **Async via Arq** — see `lacing.worker` (optional). The same
  processor can be queued through Redis when scale demands it.

Per BACK-DOC §6: Arq is preferred over Celery (lighter, async-native).
But the *processor pattern itself* is independent of Arq — most users
will never need Redis.

Example built-ins:

- [`low_confidence_review()`](_autosummary/lacing.processors.html.md#lacing.processors.low_confidence_review) — flag annotations whose confidence is
  below a threshold by adding a parallel `for-review` tier annotation.
- [`detect_density_change_points()`](_autosummary/lacing.processors.html.md#lacing.processors.detect_density_change_points) — emit point markers wherever the
  annotation density changes more than `min_delta` over a sliding
  window.

Register your own with [`register_processor()`](_autosummary/lacing.processors.html.md#lacing.processors.register_processor).

### Functions

| [`clear_processors`](_autosummary/lacing.processors.html.md#lacing.processors.clear_processors)()                             | Drop every registered processor.                                      |
|-------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------|
| [`detect_density_change_points`](_autosummary/lacing.processors.html.md#lacing.processors.detect_density_change_points)(\*, store, oplog) | Emit point markers wherever annotation density changes sharply.       |
| `get_processor`(name)                                                                           |                                                                       |
| [`low_confidence_review`](_autosummary/lacing.processors.html.md#lacing.processors.low_confidence_review)(\*, store, oplog[, ...]) | Flag low-confidence annotations by mirroring them onto a review tier. |
| [`register_processor`](_autosummary/lacing.processors.html.md#lacing.processors.register_processor)([func, name])               | Register a processor under `name` (defaults to the function name).    |
| [`registered_processors`](_autosummary/lacing.processors.html.md#lacing.processors.registered_processors)()                        | Names of every registered processor, sorted.                          |
| [`run_async`](_autosummary/lacing.processors.html.md#lacing.processors.run_async)(name, \*, store, oplog, \*\*kwargs)  | Run a processor in the current event loop.                            |
| [`run_sync`](_autosummary/lacing.processors.html.md#lacing.processors.run_sync)(name, \*, store, oplog, \*\*kwargs)   | Run a processor synchronously and return its result.                  |

### Exceptions

| [`ProcessorError`](_autosummary/lacing.processors.html.md#lacing.processors.ProcessorError)   | Raised when a registered processor's invocation fails.   |
|-------------------------------------------------------------------|----------------------------------------------------------|

### *exception* lacing.processors.ProcessorError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised when a registered processor’s invocation fails.

### lacing.processors.clear_processors()

Drop every registered processor. For tests.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *async* lacing.processors.detect_density_change_points(, store, oplog, asset_id=None, bucket_seconds=1.0, min_delta=3, rate=1000, target_tier='density-change-points', actor='processor:detect_density_change_points')

Emit point markers wherever annotation density changes sharply.

Buckets all annotations by `floor(start_seconds / bucket_seconds)`
and emits a point marker on `target_tier` for each bucket boundary
where the count differs from the previous bucket by at least
`min_delta`. Lightweight stand-in for a real CPD algorithm — the
architecture is what matters.

* **Parameters:**
  * **store** ([`Any`](https://docs.python.org/3/library/typing.html#typing.Any)) – Source store.
  * **oplog** ([`Any`](https://docs.python.org/3/library/typing.html#typing.Any)) – Where to record emitted markers.
  * **asset_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – If set, only consider annotations on this asset.
    `None` = consider all media-referenced annotations.
  * **bucket_seconds** ([`float`](https://docs.python.org/3/builtins/functions.html#float)) – Bucket width.
  * **min_delta** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Minimum 

    ```
    |Δ count|
    ```

     to emit a marker.
  * **rate** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Quantization rate for the new point intervals.
  * **target_tier** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Output tier; created if missing.
  * **actor** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Provenance attribution.
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]
* **Returns:**
  `{"markers": <count>, "target_tier": <name>}`.

### *async* lacing.processors.low_confidence_review(, store, oplog, threshold=0.5, review_tier='for-review', actor='processor:low_confidence_review')

Flag low-confidence annotations by mirroring them onto a review tier.

* **Parameters:**
  * **store** ([`Any`](https://docs.python.org/3/library/typing.html#typing.Any)) – The annotation store to scan + mutate.
  * **oplog** ([`Any`](https://docs.python.org/3/library/typing.html#typing.Any)) – Where to record the new annotations.
  * **threshold** ([`float`](https://docs.python.org/3/builtins/functions.html#float)) – Confidence below this triggers a review entry. `None`
    confidences are skipped (treated as full-confidence, since the
    user didn’t supply one).
  * **review_tier** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name of the tier to write review entries onto. Created
    if missing.
  * **actor** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Provenance `was_attributed_to` for review entries.
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]
* **Returns:**
  `{"flagged": <count>, "review_tier": <name>}`.

### lacing.processors.register_processor(func=None, , name=None)

Register a processor under `name` (defaults to the function name).

The function may be sync or async; we wrap sync funcs to a coroutine.

### lacing.processors.registered_processors()

Names of every registered processor, sorted.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### *async* lacing.processors.run_async(name, , store, oplog, \*\*kwargs)

Run a processor in the current event loop. For async callers.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### lacing.processors.run_sync(name, , store, oplog, \*\*kwargs)

Run a processor synchronously and return its result.

If the processor is async, we call it via `asyncio.run` (when no
loop is running) or schedule and wait on it (when a loop is already
active). Most callers from sync code want the former.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)


# _autosummary/lacing.quality.html.md

# lacing.quality

Inter-annotator agreement and boundary metrics.

Phase 0 ships four metrics from ANN-DOC §D, all as pure functions:

- [`cohen_kappa()`](_autosummary/lacing.quality.html.md#lacing.quality.cohen_kappa) — agreement between two annotators on a categorical label.
- [`krippendorff_alpha()`](_autosummary/lacing.quality.html.md#lacing.quality.krippendorff_alpha) — agreement across any number of annotators
  (handles missing data, multiple distance metrics).
- [`interval_iou()`](_autosummary/lacing.quality.html.md#lacing.quality.interval_iou) — Intersection-over-Union for two time intervals.
- [`boundary_iou()`](_autosummary/lacing.quality.html.md#lacing.quality.boundary_iou) — Average IoU between two sets of intervals (greedy match).

The doc cites Krippendorff thresholds: α > 0.8 reliable, 0.67–0.8 tentative,
< 0.67 discard. Kappa traditionally: >0.8 almost perfect, 0.6–0.8 substantial,
0.4–0.6 moderate.

Pure functions — no numpy dependency in Phase 0.

### Functions

| [`boundary_iou`](_autosummary/lacing.quality.html.md#lacing.quality.boundary_iou)(a, b)                              | Mean IoU between two sets of intervals via greedy best-match.   |
|--------------------------------------------------------------------------------------------------|-----------------------------------------------------------------|
| [`cohen_kappa`](_autosummary/lacing.quality.html.md#lacing.quality.cohen_kappa)(a, b)                               | Cohen's kappa for two annotators on a categorical label.        |
| [`interval_iou`](_autosummary/lacing.quality.html.md#lacing.quality.interval_iou)(a, b)                              | Intersection-over-Union for two time intervals.                 |
| [`krippendorff_alpha`](_autosummary/lacing.quality.html.md#lacing.quality.krippendorff_alpha)(annotations, \*[, distance]) | Krippendorff's α across any number of annotators.               |

### lacing.quality.boundary_iou(a, b)

Mean IoU between two sets of intervals via greedy best-match.

For each interval in `a`, finds its highest-IoU match in `b` (without
replacement — once a `b` interval is matched it’s removed from the pool).
Unmatched intervals in either set contribute 0.0 to the mean.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)
* **Returns:**
  Mean IoU ∈ [0, 1]. Returns 0.0 if both sets are empty (defensible
  as a “no agreement to measure” baseline).

### lacing.quality.cohen_kappa(a, b)

Cohen’s kappa for two annotators on a categorical label.

* **Parameters:**
  * **a** ([`Sequence`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`, bound= [`Hashable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Hashable))]) – Annotator A’s labels.
  * **b** ([`Sequence`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`, bound= [`Hashable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Hashable))]) – Annotator B’s labels (must be the same length).
* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)
* **Returns:**
  κ ∈ [-1, 1]. 1 = perfect agreement, 0 = chance, negative = worse than chance.
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If sequences differ in length or are empty.

Edge cases:
: If only one category appears across both annotators, both observed
  and expected agreement are 1.0; we return 1.0 by convention.

### lacing.quality.interval_iou(a, b)

Intersection-over-Union for two time intervals.

Returns 1.0 if both are equal point intervals at the same instant; 0.0
if they don’t intersect (including when only one is a point).

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

### lacing.quality.krippendorff_alpha(annotations, \*, distance=<function \_nominal_distance>)

Krippendorff’s α across any number of annotators.

* **Parameters:**
  * **annotations** ([`Sequence`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence)[[`Sequence`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence)[[`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`, bound= [`Hashable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Hashable))]]]) – A list of annotators, each a sequence of labels (one per
    unit). Use `None` for a missing annotation by that annotator on
    that unit.
  * **distance** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`, bound= [`Hashable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Hashable)), [`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`, bound= [`Hashable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Hashable))], [`float`](https://docs.python.org/3/builtins/functions.html#float)]) – Function `(x, y) -> float` measuring disagreement between
    two label values. Default is the nominal (0/1) distance.
* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)
* **Returns:**
  α. 1.0 = perfect agreement, 0.0 = chance.
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If sequences differ in length, fewer than 2 annotators
      given, or fewer than 2 paired observations exist.


# _autosummary/lacing.schema.html.md

# lacing.schema

Body schema registry, JSON Schema export, and migrations.

Every annotation’s `body` is validated against the schema named by its
`body_schema_uri` (semver: `annot://schema/<name>/v<major>`). This
module implements:

- [`register_body_schema()`](_autosummary/lacing.schema.html.md#lacing.schema.register_body_schema) — register a Pydantic v2 model for a schema URI.
- [`validate()`](_autosummary/lacing.schema.html.md#lacing.schema.validate) — validate an annotation’s body dict against its schema.
- [`json_schema()`](_autosummary/lacing.schema.html.md#lacing.schema.json_schema) — get the JSON Schema for a registered URI.
- [`export_json_schemas()`](_autosummary/lacing.schema.html.md#lacing.schema.export_json_schemas) — write all JSON Schema artifacts to disk
  (under `lacing/schema/<name>/v<N>.json` by default), the upstream
  for `json-schema-to-zod` codegen.
- [`register_migration()`](_autosummary/lacing.schema.html.md#lacing.schema.register_migration) — register a forward migration from version
  N to N+1 of a body schema.
- [`migrate()`](_autosummary/lacing.schema.html.md#lacing.schema.migrate) — upgrade a body dict (or whole annotation) through
  registered migrations.

See the `lacing-schema-codegen` skill and BACK-DOC §4.5.

Usage example:

```default
from pydantic import BaseModel, Field
from lacing.schema import register_body_schema, register_migration

class WordBodyV1(BaseModel):
    model_config = {"frozen": True, "extra": "forbid"}
    text: str = Field(..., description="The word's surface form.")

register_body_schema("annot://schema/word/v1", WordBodyV1)

class WordBodyV2(BaseModel):
    model_config = {"frozen": True, "extra": "forbid"}
    text: str = Field(..., description="The word's surface form.")
    normalized: str | None = Field(None, description="Normalized form.")

register_body_schema("annot://schema/word/v2", WordBodyV2)

@register_migration(schema_name="word", from_version=1, to_version=2)
def v1_to_v2(body: dict) -> dict:
    return {**body, "normalized": None}
```

### Functions

| [`clear_registry`](_autosummary/lacing.schema.html.md#lacing.schema.clear_registry)()                           | Drop every registered body schema and migration.                        |
|---------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`export_json_schemas`](_autosummary/lacing.schema.html.md#lacing.schema.export_json_schemas)(target_dir, \*[, ...]) | Write every registered schema as JSON files under `target_dir`.         |
| [`get_body_schema`](_autosummary/lacing.schema.html.md#lacing.schema.get_body_schema)(uri)                       | Look up the registered Pydantic model for `uri`.                        |
| `is_registered`(uri)                                                                        |                                                                         |
| [`json_schema`](_autosummary/lacing.schema.html.md#lacing.schema.json_schema)(uri)                           | Return the JSON Schema for the body model registered at `uri`.          |
| [`latest_version`](_autosummary/lacing.schema.html.md#lacing.schema.latest_version)(schema_name)                | Highest registered major version for `schema_name`, or None.            |
| [`make_uri`](_autosummary/lacing.schema.html.md#lacing.schema.make_uri)(name, version)                    | Build a `body_schema_uri` from a name and major version.                |
| [`migrate`](_autosummary/lacing.schema.html.md#lacing.schema.migrate)(body, \*, from_uri, to_uri)        | Migrate `body` from `from_uri` to `to_uri` via registered steps.        |
| [`migrate_to_latest`](_autosummary/lacing.schema.html.md#lacing.schema.migrate_to_latest)(body, \*, from_uri)      | Convenience: migrate to the highest registered version of the schema.   |
| [`parse_uri`](_autosummary/lacing.schema.html.md#lacing.schema.parse_uri)(uri)                             | Split `annot://schema/<name>/v<major>` into `(name, major)`.            |
| [`register_body_schema`](_autosummary/lacing.schema.html.md#lacing.schema.register_body_schema)(uri, model)           | Register `model` as the validator for `uri`.                            |
| [`register_migration`](_autosummary/lacing.schema.html.md#lacing.schema.register_migration)(\*, schema_name, ...)   | Register a forward migration from `v<from_version>` to `v<to_version>`. |
| [`registered_uris`](_autosummary/lacing.schema.html.md#lacing.schema.registered_uris)()                          | All currently registered `body_schema_uri` values, sorted.              |
| [`validate`](_autosummary/lacing.schema.html.md#lacing.schema.validate)(body, uri)                        | Validate `body` against the schema registered for `uri`.                |

### Exceptions

| [`BodySchemaError`](_autosummary/lacing.schema.html.md#lacing.schema.BodySchemaError)          | Raised when a body fails validation against its registered schema.       |
|---------------------------------------------------------------------------|--------------------------------------------------------------------------|
| [`EmptySchemaRegistryError`](_autosummary/lacing.schema.html.md#lacing.schema.EmptySchemaRegistryError) | Raised when an export would write an empty registry over real artifacts. |
| [`MigrationError`](_autosummary/lacing.schema.html.md#lacing.schema.MigrationError)           | Raised when a migration step is missing or fails.                        |
| [`UnknownBodySchemaError`](_autosummary/lacing.schema.html.md#lacing.schema.UnknownBodySchemaError)   | Raised when an annotation's body_schema_uri has no registered model.     |

### *exception* lacing.schema.BodySchemaError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

Raised when a body fails validation against its registered schema.

### *exception* lacing.schema.EmptySchemaRegistryError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised when an export would write an empty registry over real artifacts.

Almost always means the caller forgot `import lacing.bodies`: the
registry is populated by importing the body modules, so exporting without
that import silently truncates `index.json` to `{}` and leaves the
committed `v<N>.json` files orphaned.

### *exception* lacing.schema.MigrationError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised when a migration step is missing or fails.

### *exception* lacing.schema.UnknownBodySchemaError

Bases: [`KeyError`](https://docs.python.org/3/builtins/exceptions.html#KeyError)

Raised when an annotation’s body_schema_uri has no registered model.

### lacing.schema.clear_registry()

Drop every registered body schema and migration. For tests / repls.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### lacing.schema.export_json_schemas(target_dir, , overwrite=True, include_meta=True, allow_empty=False)

Write every registered schema as JSON files under `target_dir`.

Layout: `<target_dir>/<name>/v<N>.json`. Returns the list of paths
written, in registration order.

* **Parameters:**
  * **target_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Output directory. Created if missing.
  * **overwrite** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If False, refuse to write a file that already exists.
  * **include_meta** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, also write a `<target_dir>/index.json`
    mapping every URI to its file path and the Pydantic model’s
    qualified name (helps the codegen pipeline).
  * **allow_empty** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, permit an export from an empty registry. The
    default refuses, because the overwhelmingly likely cause is a
    missing `import lacing.bodies` and the write would destroy the
    committed artifacts.
* **Raises:**
  [**EmptySchemaRegistryError**](_autosummary/lacing.schema.html.md#lacing.schema.EmptySchemaRegistryError) – Nothing is registered and `allow_empty`
      is False.
* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]

### lacing.schema.get_body_schema(uri)

Look up the registered Pydantic model for `uri`.

* **Return type:**
  [`type`](https://docs.python.org/3/builtins/functions.html#type)[`BaseModel`]

### lacing.schema.json_schema(uri)

Return the JSON Schema for the body model registered at `uri`.

Pydantic’s `model_json_schema()` output, plus a `default` for every
zero-argument `default_factory` field (see
`lacing.json_schema.DefaultFactoryJsonSchema`), whatever base class
the registered model has.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### lacing.schema.latest_version(schema_name)

Highest registered major version for `schema_name`, or None.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### lacing.schema.make_uri(name, version)

Build a `body_schema_uri` from a name and major version.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### lacing.schema.migrate(body, , from_uri, to_uri)

Migrate `body` from `from_uri` to `to_uri` via registered steps.

Composes single-step migrations. Raises [`MigrationError`](_autosummary/lacing.schema.html.md#lacing.schema.MigrationError) if any
step is missing.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### lacing.schema.migrate_to_latest(body, , from_uri)

Convenience: migrate to the highest registered version of the schema.

Returns `(migrated_body, target_uri)`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### lacing.schema.parse_uri(uri)

Split `annot://schema/<name>/v<major>` into `(name, major)`.

Raises `ValueError` on malformed input.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`int`](https://docs.python.org/3/builtins/functions.html#int)]

### lacing.schema.register_body_schema(uri, model)

Register `model` as the validator for `uri`. Returns `model`.

* **Return type:**
  [`type`](https://docs.python.org/3/builtins/functions.html#type)[`BaseModel`]

### lacing.schema.register_migration(, schema_name, from_version, to_version)

Register a forward migration from `v<from_version>` to `v<to_version>`.

Decorated function takes a body `dict` and returns a new body `dict`.
Migrations must be one major-version step at a time
(`to_version == from_version + 1`).

Re-registering the same `(schema_name, from_version)` pair replaces
the previous entry — convenient in tests, intentional for hot-reload.

### lacing.schema.registered_uris()

All currently registered `body_schema_uri` values, sorted.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### lacing.schema.validate(body, uri)

Validate `body` against the schema registered for `uri`.

Returns the parsed Pydantic instance. Raises [`BodySchemaError`](_autosummary/lacing.schema.html.md#lacing.schema.BodySchemaError)
on validation failure (wrapping the underlying `pydantic.ValidationError`)
or [`UnknownBodySchemaError`](_autosummary/lacing.schema.html.md#lacing.schema.UnknownBodySchemaError) if the URI isn’t registered.

* **Return type:**
  `BaseModel`


# _autosummary/lacing.store.base.html.md

# lacing.store.base

`IntervalAnnotationStore` — the headline Pythonic API.

A `MutableMapping[TimeInterval, list[Annotation]]` facade with methods
named after Allen’s relations. See ANN-DOC §C (“a natural design target”)
and BACK-DOC §4.1.

The facade hides the index. Phase 0 ships `MemoryStore` over
`intervaltree`. Phase 1 adds SQLite + R\*Tree (`.annot`) and PostgreSQL

+ `tstzrange` GiST. The mapping interface is the same in all three.

### Classes

| [`IntervalAnnotationStore`](_autosummary/lacing.store.base.html.md#lacing.store.base.IntervalAnnotationStore)(\*args, \*\*kwargs)   | Protocol for any interval-keyed annotation store.   |
|------------------------------------------------------------------------------------------------|-----------------------------------------------------|

### *class* lacing.store.base.IntervalAnnotationStore(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

Protocol for any interval-keyed annotation store.

Conceptually a `MutableMapping[TimeInterval, list[Annotation]]`: keys
are `TimeInterval`; values are lists because multiple annotations can
share an interval (different tiers, multiple annotators, soft labels).

We use `Protocol` rather than inheriting from `MutableMapping` so
backends (in-memory, SQLite, Postgres) can structurally conform without
forcing a single class hierarchy. The mapping methods below match the
`MutableMapping` ABC; concrete backends like `MemoryStore`
implement the full interface.

#### add(annotation)

Append `annotation` to the list at its reference interval.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### all()

Iterate every annotation in the store, order unspecified.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### at_tier(tier_name, query)

Annotations on `tier_name` that intersect `query`.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### by_tier(tier_name)

All annotations on `tier_name`, regardless of interval.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### contains(query)

Annotations whose interval strictly contains `query` (Allen `di`).

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### during(query)

Annotations whose interval is strictly inside `query` (Allen `d`).

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### equals(query)

Allen `=`: identical interval.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### extend(annotations)

Add many; equivalent to repeated `.add` but adapters can optimize.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### finishes(query)

Allen `f`: later start, same end.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### intersects(query)

Annotations whose interval shares any time with `query`.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### lock(key)

Exclusive lock named `key`, held for the `with` block.

Scoped to this store’s data (for `PostgresStore`, its owner and
project): holders of the same key exclude each other, different keys do
not. The reach of the exclusion is the backend’s: `MemoryStore`
excludes threads of this process, `SqliteStore` also other processes
on the host, `PostgresStore` every client of the database, on any
host. A holder that dies releases the lock. Not re-entrant.

* **Return type:**
  [`AbstractContextManager`](https://docs.python.org/3/library/contextlib.html#contextlib.AbstractContextManager)[[`None`](https://docs.python.org/3/builtins/constants.html#None)]

#### meets(query)

Allen `m`: `a.end == q.start`.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### overlaps(query)

Strict Allen `o`: `a.start < q.start < a.end < q.end`.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### relate(query, relations)

Annotations whose interval has any of the named `relations` to `query`.

Generic dispatch — useful when relations are computed at runtime.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### remove(annotation_id)

Remove and return the annotation with this id, or None if absent.

* **Return type:**
  [`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### starts(query)

Allen `s`: same start, earlier end.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### tiers()

Registered tiers. Annotations may reference tiers not yet registered;
callers decide whether that’s an error.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Tier`](_autosummary/lacing.tier.html.md#lacing.tier.Tier)]

#### update(annotation)

Replace the stored annotation that has `annotation.id`, atomically.

One commit: a concurrent reader sees the old annotation or the new one,
never neither (`remove(id)` then `add(...)` leaves a gap in
between). The replacement may move to a different interval or tier.
Raises `KeyError` when no annotation has that id — use [`add()`](_autosummary/lacing.store.base.html.md#lacing.store.base.IntervalAnnotationStore.add)
for a new one.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/lacing.store.html.md

# lacing.store

Interval-keyed annotation stores.

Public surface: `IntervalAnnotationStore` (the facade), `MemoryStore`,
`MappingStore` (persisted to any injected `MutableMapping`),
`SqliteStore` (the `.annot` on-disk format), the optional
`PostgresStore`, and the store-schema migration ladder
([`lacing.store.migrations`](_autosummary/lacing.store.migrations.html.md#module-lacing.store.migrations)).

### Functions

| [`register_store_migration`](_autosummary/lacing.store.html.md#lacing.store.register_store_migration)(\*, store_kind, ...)   | Register a forward store migration from `from_version` to `to_version`.   |
|--------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|
| [`migrate_annot_file`](_autosummary/lacing.store.html.md#lacing.store.migrate_annot_file)(path, \*[, to_version])      | Migrate a `.annot` file in place, returning `(from, to)` versions.        |
| [`reachable_versions`](_autosummary/lacing.store.html.md#lacing.store.reachable_versions)(store_kind, from_version)    | Versions reachable from `from_version` by chaining registered steps.      |
| [`rebuild_annotations_rtree`](_autosummary/lacing.store.html.md#lacing.store.rebuild_annotations_rtree)(conn)                 | Rebuild the interval index from the `annotations` table, in place.        |
| [`get_pool`](_autosummary/lacing.store.html.md#lacing.store.get_pool)(connection_string, \*\*pool_kwargs)    | Return a process-wide `psycopg_pool.ConnectionPool` for a conninfo.       |
| [`close_all_pools`](_autosummary/lacing.store.html.md#lacing.store.close_all_pools)()                               | Close every registered connection pool (test teardown / shutdown).        |

### Classes

| [`IntervalAnnotationStore`](_autosummary/lacing.store.html.md#lacing.store.IntervalAnnotationStore)(\*args, \*\*kwargs)       | Protocol for any interval-keyed annotation store.                         |
|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|
| [`MemoryStore`](_autosummary/lacing.store.html.md#lacing.store.MemoryStore)()                                     | `IntervalAnnotationStore` implementation over `intervaltree`.             |
| [`MappingStore`](_autosummary/lacing.store.html.md#lacing.store.MappingStore)(mapping, \*[, codec, tiers_key])     | An `IntervalAnnotationStore` persisted to an injected `MutableMapping`.   |
| [`MappingCodec`](_autosummary/lacing.store.html.md#lacing.store.MappingCodec)(encode, decode)                      | How a JSON-ready `dict` is turned into what the mapping stores, and back. |
| [`SqliteStore`](_autosummary/lacing.store.html.md#lacing.store.SqliteStore)(path, \*[, check_same_thread, ...])   | SQLite-backed `IntervalAnnotationStore`.                                  |
| [`PostgresStore`](_autosummary/lacing.store.html.md#lacing.store.PostgresStore)(connection_string, \*[, rate, ...]) | PostgreSQL-backed `IntervalAnnotationStore`, scoped to one tenant.        |

### Exceptions

| [`SchemaMismatchError`](_autosummary/lacing.store.html.md#lacing.store.SchemaMismatchError)   | Raised when opening a `.annot` file with an incompatible schema.                |
|------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| [`StoreMigrationError`](_autosummary/lacing.store.html.md#lacing.store.StoreMigrationError)   | Raised when a store migration step is missing or fails.                         |
| [`PgSchemaMismatchError`](_autosummary/lacing.store.html.md#lacing.store.PgSchemaMismatchError) | Raised on opening a database whose schema_version differs.                      |
| [`TierOverlapError`](_autosummary/lacing.store.html.md#lacing.store.TierOverlapError)      | Raised when an insert would violate a per-tier no-overlap constraint.           |
| [`RateMismatchError`](_autosummary/lacing.store.html.md#lacing.store.RateMismatchError)     | Raised when an annotation's interval cannot be represented at the project rate. |

### *class* lacing.store.IntervalAnnotationStore(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

Protocol for any interval-keyed annotation store.

Conceptually a `MutableMapping[TimeInterval, list[Annotation]]`: keys
are `TimeInterval`; values are lists because multiple annotations can
share an interval (different tiers, multiple annotators, soft labels).

We use `Protocol` rather than inheriting from `MutableMapping` so
backends (in-memory, SQLite, Postgres) can structurally conform without
forcing a single class hierarchy. The mapping methods below match the
`MutableMapping` ABC; concrete backends like [`MemoryStore`](_autosummary/lacing.store.html.md#lacing.store.MemoryStore)
implement the full interface.

#### add(annotation)

Append `annotation` to the list at its reference interval.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### all()

Iterate every annotation in the store, order unspecified.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### at_tier(tier_name, query)

Annotations on `tier_name` that intersect `query`.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### by_tier(tier_name)

All annotations on `tier_name`, regardless of interval.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### contains(query)

Annotations whose interval strictly contains `query` (Allen `di`).

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### during(query)

Annotations whose interval is strictly inside `query` (Allen `d`).

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### equals(query)

Allen `=`: identical interval.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### extend(annotations)

Add many; equivalent to repeated `.add` but adapters can optimize.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### finishes(query)

Allen `f`: later start, same end.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### intersects(query)

Annotations whose interval shares any time with `query`.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### lock(key)

Exclusive lock named `key`, held for the `with` block.

Scoped to this store’s data (for `PostgresStore`, its owner and
project): holders of the same key exclude each other, different keys do
not. The reach of the exclusion is the backend’s: `MemoryStore`
excludes threads of this process, `SqliteStore` also other processes
on the host, `PostgresStore` every client of the database, on any
host. A holder that dies releases the lock. Not re-entrant.

* **Return type:**
  [`AbstractContextManager`](https://docs.python.org/3/library/contextlib.html#contextlib.AbstractContextManager)[[`None`](https://docs.python.org/3/builtins/constants.html#None)]

#### meets(query)

Allen `m`: `a.end == q.start`.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### overlaps(query)

Strict Allen `o`: `a.start < q.start < a.end < q.end`.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### relate(query, relations)

Annotations whose interval has any of the named `relations` to `query`.

Generic dispatch — useful when relations are computed at runtime.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### remove(annotation_id)

Remove and return the annotation with this id, or None if absent.

* **Return type:**
  [`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### starts(query)

Allen `s`: same start, earlier end.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### tiers()

Registered tiers. Annotations may reference tiers not yet registered;
callers decide whether that’s an error.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`Tier`](_autosummary/lacing.tier.html.md#lacing.tier.Tier)]

#### update(annotation)

Replace the stored annotation that has `annotation.id`, atomically.

One commit: a concurrent reader sees the old annotation or the new one,
never neither (`remove(id)` then `add(...)` leaves a gap in
between). The replacement may move to a different interval or tier.
Raises `KeyError` when no annotation has that id — use [`add()`](_autosummary/lacing.store.html.md#lacing.store.IntervalAnnotationStore.add)
for a new one.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* lacing.store.MappingCodec(encode, decode)

Bases: [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)

How a JSON-ready `dict` is turned into what the mapping stores, and back.

#### decode *: [Callable](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[Any](https://docs.python.org/3/library/typing.html#typing.Any)], [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)]*

Alias for field number 1

#### encode *: [Callable](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[dict](https://docs.python.org/3/builtins/stdtypes.html#dict)], [Any](https://docs.python.org/3/library/typing.html#typing.Any)]*

Alias for field number 0

### *class* lacing.store.MappingStore(mapping, \*, codec=(<function <lambda>>, <function <lambda>>), tiers_key='_\_lacing_tiers_\_')

Bases: [`MemoryStore`](_autosummary/lacing.store.memory.html.md#lacing.store.memory.MemoryStore)

An `IntervalAnnotationStore` persisted to an injected `MutableMapping`.

```pycon
>>> from lacing.store import MappingStore
>>> backing = {}
>>> store = MappingStore(backing)
>>> len(backing)  # nothing written until something is added
0
```

Reopening over the same mapping sees the same annotations and tiers.

Raises `ValueError` on a duplicate annotation id (as `SqliteStore`
does) and when an annotation is assigned under a key that is not its own
interval; both would otherwise not survive a reload.

#### close()

No-op. Writes go through immediately and the mapping is the caller’s.

Present so code that calls `store.close()` after each use (as it does
for `SqliteStore`) can treat both alike; the store stays usable.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* lacing.store.MemoryStore

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

`IntervalAnnotationStore` implementation over `intervaltree`.

Conforms to the protocol in `lacing.store.base`. We don’t formally
inherit from `IntervalAnnotationStore` because it’s a `Protocol`
with method bodies — structural typing is enough.

#### lock(key)

Exclusive lock named `key` among the threads of this process.

* **Return type:**
  [`AbstractContextManager`](https://docs.python.org/3/library/contextlib.html#contextlib.AbstractContextManager)[[`None`](https://docs.python.org/3/builtins/constants.html#None)]

### *exception* lacing.store.PgSchemaMismatchError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised on opening a database whose schema_version differs.

### *class* lacing.store.PostgresStore(connection_string, , rate=24000, owner_id='default', project_id='default', autocommit=True, use_pool=True)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

PostgreSQL-backed `IntervalAnnotationStore`, scoped to one tenant.

A store instance is bound to one `(owner_id, project_id)` pair; multiple
instances over the same database (different project ids) coexist without
seeing each other’s annotations or tiers (see the module docstring on
multi-tenancy). The mapping/Allen/tier surface is unchanged — the tenant
scoping is transparent.

* **Parameters:**
  * **connection_string** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)) – A psycopg-compatible conninfo URL, or a kwargs dict
    (`host`/`port`/`user`/`password`/`dbname`).
  * **rate** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Project-wide rate for *this* `(owner, project)`. Set on first
    init for the pair; re-opening the same pair with a different rate
    raises [`PgSchemaMismatchError`](_autosummary/lacing.store.html.md#lacing.store.PgSchemaMismatchError). Different projects in the
    same DB may have different rates.
  * **owner_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Tenant owner. Defaults to `DEFAULT_OWNER_ID` — the
    forward seam for the access layer (reelee#174); enforcement deferred.
  * **project_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Logical project key. Defaults to `DEFAULT_PROJECT_ID`.
    `nw` passes the project’s `project_asset_id` here.
  * **autocommit** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True (default), statements run in their own transaction.
  * **use_pool** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – When True (default) and the connection is given as a string,
    borrow a connection from a process-wide pool keyed by the conninfo
    (amortizes per-op connect cost; see [`get_pool()`](_autosummary/lacing.store.html.md#lacing.store.get_pool)). Falls back to
    a dedicated connection when `psycopg_pool` is unavailable, when the
    connection is given as a dict, or when `use_pool=False`.

#### add_tier(tier, , enforce_no_overlap=False)

Add or update a tier.

* **Parameters:**
  * **tier** ([`Tier`](_autosummary/lacing.tier.html.md#lacing.tier.Tier)) – Tier definition.
  * **enforce_no_overlap** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, install a per-tier `EXCLUDE USING
    GIST` constraint forbidding overlapping annotations within
    this tier. Required for proper TIME_SUBDIVISION enforcement.
    Cannot be toggled after the tier has annotations.
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### lock(key)

Exclusive lock named `key` for this `(owner, project)`, across hosts.

A session-level `pg_advisory_lock` keyed on a 64-bit hash of
`owner_id` + `project_id` + `key`, taken on a dedicated connection
(never the store’s own, which other threads and transactions share) and
released in `finally`. If the holder crashes, Postgres drops the lock
when its connection dies, so nothing can go stale. Blocks until
available; not re-entrant.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`None`](https://docs.python.org/3/builtins/constants.html#None)]

#### update(annotation)

Replace the row with this id in ONE `UPDATE` statement (lacing#66).

A single statement is atomic in any isolation mode, so a concurrent
reader sees the old row or the new one — unlike `remove` then `add`,
which commit twice. Raises `KeyError` if the id is not in this
`(owner, project)` and `TierOverlapError` like `add()`.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *exception* lacing.store.RateMismatchError

Bases: [`LossyTimeConversionError`](_autosummary/lacing.time.html.md#lacing.time.LossyTimeConversionError)

Raised when an annotation’s interval cannot be represented at the project rate.

### *exception* lacing.store.SchemaMismatchError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised when opening a `.annot` file with an incompatible schema.

### *class* lacing.store.SqliteStore(path, , check_same_thread=True, migrate=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

SQLite-backed `IntervalAnnotationStore`.

* **Parameters:**
  * **path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike)) – Path to the `.annot` file. Use `":memory:"` for an
    ephemeral in-memory database.
  * **check_same_thread** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Forwarded to `sqlite3.connect`. We hold a
    single connection guarded by a lock; pass `False` when
    sharing across threads.
  * **migrate** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Opt-in to upgrading a file written at an older
    `schema_version` on open, via the ladder in
    [`lacing.store.migrations`](_autosummary/lacing.store.migrations.html.md#module-lacing.store.migrations). Off by default — silently
    rewriting someone’s file on open is worse than refusing.
    Every open-time schema failure — refusal *or* failed migration
    — raises [`SchemaMismatchError`](_autosummary/lacing.store.html.md#lacing.store.SchemaMismatchError); a failed migration
    chains the ladder’s `StoreMigrationError` as its cause.

#### lock(key)

Exclusive lock named `key`: a file lock beside the database.

Excludes other threads and other processes on this host that open the
same `.annot` file; the kernel frees it if the holder dies. An
in-memory database (`":memory:"`) has no file, so it falls back to a
process-local lock.

* **Return type:**
  [`AbstractContextManager`](https://docs.python.org/3/library/contextlib.html#contextlib.AbstractContextManager)[[`None`](https://docs.python.org/3/builtins/constants.html#None)]

#### update(annotation)

Replace the row with this id (and its R\*Tree entry) in one transaction.

Readers take the same lock (`_fetchall()`), so on a connection
shared between threads — where a reader would otherwise see this
transaction’s uncommitted state — they get the old annotation or the
new one, never neither (lacing#66).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *exception* lacing.store.StoreMigrationError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised when a store migration step is missing or fails.

### *exception* lacing.store.TierOverlapError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised when an insert would violate a per-tier no-overlap constraint.

### lacing.store.close_all_pools()

Close every registered connection pool (test teardown / shutdown).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### lacing.store.get_pool(connection_string, \*\*pool_kwargs)

Return a process-wide `psycopg_pool.ConnectionPool` for a conninfo.

One pool per distinct `connection_string` (the keying happens here, not at
the call-site), created lazily on first request. Raises [`ImportError`](https://docs.python.org/3/builtins/exceptions.html#ImportError)
if `psycopg_pool` is not installed — callers that want a graceful fallback
catch it (see [`PostgresStore`](_autosummary/lacing.store.html.md#lacing.store.PostgresStore)).

* **Parameters:**
  * **connection_string** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – psycopg conninfo URL (the registry key).
  * **\*\*pool_kwargs** – forwarded to `ConnectionPool` on
    first creation (e.g. `min_size`, `max_size`); ignored on reuse.
* **Returns:**
  The shared pool for this conninfo.

### lacing.store.migrate_annot_file(path, , to_version=None)

Migrate a `.annot` file in place, returning `(from, to)` versions.

`to_version` defaults to the current build’s
[`lacing.store.sqlite.SCHEMA_VERSION`](_autosummary/lacing.store.sqlite.html.md#lacing.store.sqlite.SCHEMA_VERSION). Already-current files are a
no-op (`from == to`). Each step runs in its own `BEGIN IMMEDIATE`
transaction with the version re-checked under the lock, so concurrent
migrators converge and an interrupted chain resumes from the last
version that completed (idempotent).

Raises [`StoreMigrationError`](_autosummary/lacing.store.html.md#lacing.store.StoreMigrationError) when the file does not exist, is
not a `.annot` file, a step is missing, fails, or breaks one of the
runner’s in-transaction guarantees.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`int`](https://docs.python.org/3/builtins/functions.html#int)]

### lacing.store.reachable_versions(store_kind, from_version)

Versions reachable from `from_version` by chaining registered steps.

Ascending, excluding `from_version` itself. Empty when no step leaves
`from_version` — which is what a refusal message should say out loud
instead of the bare “run a migration” it used to say.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### lacing.store.rebuild_annotations_rtree(conn)

Rebuild the interval index from the `annotations` table, in place.

For use *inside* a migration step after a table rebuild. Reproduces the
store’s ULP-widening contract (bounds widened by one float ULP so
float→exact-bound comparisons never drop hits — see
[`lacing.store.sqlite`](_autosummary/lacing.store.sqlite.html.md#module-lacing.store.sqlite)). Returns the number of rows indexed.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

### lacing.store.register_store_migration(, store_kind, from_version, to_version)

Register a forward store migration from `from_version` to `to_version`.

The decorated function takes the backend’s open connection and must
perform every change of the step — DDL, row rewrites, and the
`meta.schema_version` write. Steps must be one version at a time
(`to_version == from_version + 1`); the runner chains them.

Step authors: read the module docstring’s rules — no `executescript`
/ `COMMIT` / `ROLLBACK` inside a step, preserve rowids on table
rebuilds, and rebuild the interval index with
[`rebuild_annotations_rtree()`](_autosummary/lacing.store.html.md#lacing.store.rebuild_annotations_rtree) if the `annotations` table was
rebuilt.

Re-registering the same `(store_kind, from_version)` pair replaces the
previous entry.

### Modules

| [`base`](_autosummary/lacing.store.base.html.md#module-lacing.store.base)             | `IntervalAnnotationStore` — the headline Pythonic API.                           |
|--------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|
| [`locking`](_autosummary/lacing.store.locking.html.md#module-lacing.store.locking)       | Named exclusive locks for stores: the mechanisms behind `store.lock(key)`.       |
| [`mapping`](_autosummary/lacing.store.mapping.html.md#module-lacing.store.mapping)       | `MappingStore` — an `IntervalAnnotationStore` persisted to any `MutableMapping`. |
| [`memory`](_autosummary/lacing.store.memory.html.md#module-lacing.store.memory)         | In-memory annotation store backed by an interval tree.                           |
| [`migrations`](_autosummary/lacing.store.migrations.html.md#module-lacing.store.migrations) | Store-level schema migrations — the on-disk counterpart of the body ladder.      |
| [`postgres`](_autosummary/lacing.store.postgres.html.md#module-lacing.store.postgres)     | PostgreSQL-backed annotation store using `int8range` + GiST.                     |
| [`sqlite`](_autosummary/lacing.store.sqlite.html.md#module-lacing.store.sqlite)         | SQLite-backed annotation store and the `.annot` portable file format.            |


# _autosummary/lacing.store.locking.html.md

# lacing.store.locking

Named exclusive locks for stores: the mechanisms behind `store.lock(key)`.

`IntervalAnnotationStore.lock(key)` is an exclusive context manager scoped to
one store (for `PostgresStore`: one `(owner, project)`). Each backend picks
the narrowest mechanism that excludes every holder that can reach the same
data: a process-local lock for `MemoryStore`, a file lock beside the database
for `SqliteStore` (excludes other processes on the host too), a Postgres
advisory lock for `PostgresStore` (excludes other hosts).

Locks are *not* re-entrant: taking a lock you already hold blocks forever.

### Module Attributes

| [`KEY_DIGEST_BYTES`](_autosummary/lacing.store.locking.html.md#lacing.store.locking.KEY_DIGEST_BYTES)   | Bytes of the blake2b digest used to name a key (file name / advisory id).   |
|---------------------------------------------------------------------|-----------------------------------------------------------------------------|

### Functions

| [`advisory_lock_id`](_autosummary/lacing.store.locking.html.md#lacing.store.locking.advisory_lock_id)(\*parts)       | A signed 64-bit integer for `parts`: the argument of `pg_advisory_lock`.     |
|----------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| [`file_lock`](_autosummary/lacing.store.locking.html.md#lacing.store.locking.file_lock)(path)                 | Exclusive lock on `path` (created if missing), held for the `with` block.    |
| [`key_digest`](_autosummary/lacing.store.locking.html.md#lacing.store.locking.key_digest)(\*parts)             | Stable digest of `parts` (length-prefixed, so `("a", "bc") != ("ab", "c")`). |
| [`sidecar_lock_path`](_autosummary/lacing.store.locking.html.md#lacing.store.locking.sidecar_lock_path)(db_path, key) | Lock file for `key` beside the database at `db_path`.                        |

### Classes

| [`NamedLocks`](_autosummary/lacing.store.locking.html.md#lacing.store.locking.NamedLocks)()   | Process-local named exclusive locks (a registry of `threading.Lock`).   |
|-----------------------------------------------------------------|-------------------------------------------------------------------------|

### lacing.store.locking.KEY_DIGEST_BYTES *= 8*

Bytes of the blake2b digest used to name a key (file name / advisory id).

### *class* lacing.store.locking.NamedLocks

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Process-local named exclusive locks (a registry of `threading.Lock`).

### lacing.store.locking.advisory_lock_id(\*parts)

A signed 64-bit integer for `parts`: the argument of `pg_advisory_lock`.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

```pycon
>>> advisory_lock_id("o", "p", "k") == advisory_lock_id("o", "p", "k")
True
>>> advisory_lock_id("o", "p", "k") != advisory_lock_id("o", "p", "other")
True
>>> -2**63 <= advisory_lock_id("o", "p", "k") < 2**63
True
```

### lacing.store.locking.file_lock(path)

Exclusive lock on `path` (created if missing), held for the `with` block.

A fresh file descriptor per call, so threads of one process exclude each
other as well as other processes. The kernel drops the lock if the holder
dies. The lock file is left in place (removing it would race a waiter).

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`None`](https://docs.python.org/3/builtins/constants.html#None)]

### lacing.store.locking.key_digest(\*parts)

Stable digest of `parts` (length-prefixed, so `("a", "bc") != ("ab", "c")`).

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

### lacing.store.locking.sidecar_lock_path(db_path, key)

Lock file for `key` beside the database at `db_path`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/lacing.store.mapping.html.md

# lacing.store.mapping

`MappingStore` — an `IntervalAnnotationStore` persisted to any `MutableMapping`.

Where [`SqliteStore`](_autosummary/lacing.store.sqlite.html.md#lacing.store.sqlite.SqliteStore) owns a file, `MappingStore`
owns nothing: the caller injects the persistence as a `MutableMapping` (a
plain `dict`, a `dol` `JsonFiles` / `Files` store, an application’s
“mall” entry, an S3-backed mapping, …). An app that already keeps its data
in mappings can hand one to lacing as its annotation graph, so there is no
second persistence path beside the one it already has.

## Layout inside the mapping

* **One key per annotation**: `str(annotation.id)` (a UUID string) ->
  `annotation.model_dump(mode="json")`, the same JSON shape every other
  lacing surface (server, MCP, op-log) uses for an `Annotation`.
* **Tiers under one reserved key** (`tiers_key`, default
  `"__lacing_tiers__"`): `{"tiers": [<Tier.to_wire()>, ...]}`. The key
  can never collide with an annotation key because a UUID string is exactly
  36 characters of hex and hyphens.

The mapping holds **JSON-ready dicts** by default. That is what a plain
`dict` and `dol`’s `JsonFiles` both want (`JsonFiles` does the
`json.dumps` itself). For a mapping that stores `bytes` or `str` (a raw
`Files` store, a blob bucket), pass `codec=JSON_BYTES_CODEC` or
`JSON_STR_CODEC`, or your own [`MappingCodec`](_autosummary/lacing.store.mapping.html.md#lacing.store.mapping.MappingCodec).

The interval index is [`MemoryStore`](_autosummary/lacing.store.memory.html.md#lacing.store.memory.MemoryStore)’s: the whole
mapping is loaded into it on construction and every mutation is written
through to the mapping *first* and applied to the index only if that write
succeeded. All Allen-relation queries are the in-memory ones; there is no
re-implementation here.

## Lifecycle

`close()` is a harmless no-op that leaves the store usable (write-through
means there is nothing to flush, and the mapping is not ours to close).

## Concurrency

**Single writer, no locking.** Unlike `SqliteStore` there is no cross-process
lock: the index is a snapshot taken at construction, so a second
`MappingStore` over the same mapping (in this or another process) does not
see the first one’s later writes, and two writers can overwrite each other’s
tier registry. Serialise writers yourself, or open one store per process
lifetime. Keys foreign to the layout (anything that is neither a UUID string
nor `tiers_key`) make construction raise rather than be silently skipped, so
point the store at a mapping (or a sub-mapping) dedicated to it.

### Module Attributes

| [`DICT_CODEC`](_autosummary/lacing.store.mapping.html.md#lacing.store.mapping.DICT_CODEC)       | Store the JSON-ready dict itself (`dict`, dol `JsonFiles`).        |
|-------------------------------------------------------------------|--------------------------------------------------------------------|
| [`JSON_BYTES_CODEC`](_autosummary/lacing.store.mapping.html.md#lacing.store.mapping.JSON_BYTES_CODEC) | Store UTF-8 JSON `bytes` (a raw dol `Files` store, a blob bucket). |
| [`JSON_STR_CODEC`](_autosummary/lacing.store.mapping.html.md#lacing.store.mapping.JSON_STR_CODEC)   | Store JSON `str`.                                                  |

### Classes

| [`MappingCodec`](_autosummary/lacing.store.mapping.html.md#lacing.store.mapping.MappingCodec)(encode, decode)                  | How a JSON-ready `dict` is turned into what the mapping stores, and back.   |
|------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| [`MappingStore`](_autosummary/lacing.store.mapping.html.md#lacing.store.mapping.MappingStore)(mapping, \*[, codec, tiers_key]) | An `IntervalAnnotationStore` persisted to an injected `MutableMapping`.     |

### lacing.store.mapping.DICT_CODEC *= (<function <lambda>>, <function <lambda>>)*

Store the JSON-ready dict itself (`dict`, dol `JsonFiles`). The default.

### lacing.store.mapping.JSON_BYTES_CODEC *= (<function <lambda>>, <function <lambda>>)*

Store UTF-8 JSON `bytes` (a raw dol `Files` store, a blob bucket).

### lacing.store.mapping.JSON_STR_CODEC *= (<function dumps>, <function <lambda>>)*

Store JSON `str`.

### *class* lacing.store.mapping.MappingCodec(encode, decode)

Bases: [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)

How a JSON-ready `dict` is turned into what the mapping stores, and back.

#### decode *: [Callable](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[Any](https://docs.python.org/3/library/typing.html#typing.Any)], [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)]*

Alias for field number 1

#### encode *: [Callable](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[dict](https://docs.python.org/3/builtins/stdtypes.html#dict)], [Any](https://docs.python.org/3/library/typing.html#typing.Any)]*

Alias for field number 0

### *class* lacing.store.mapping.MappingStore(mapping, \*, codec=(<function <lambda>>, <function <lambda>>), tiers_key='_\_lacing_tiers_\_')

Bases: [`MemoryStore`](_autosummary/lacing.store.memory.html.md#lacing.store.memory.MemoryStore)

An `IntervalAnnotationStore` persisted to an injected `MutableMapping`.

```pycon
>>> from lacing.store import MappingStore
>>> backing = {}
>>> store = MappingStore(backing)
>>> len(backing)  # nothing written until something is added
0
```

Reopening over the same mapping sees the same annotations and tiers.

Raises `ValueError` on a duplicate annotation id (as `SqliteStore`
does) and when an annotation is assigned under a key that is not its own
interval; both would otherwise not survive a reload.

#### close()

No-op. Writes go through immediately and the mapping is the caller’s.

Present so code that calls `store.close()` after each use (as it does
for `SqliteStore`) can treat both alike; the store stays usable.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/lacing.store.memory.html.md

# lacing.store.memory

In-memory annotation store backed by an interval tree.

Uses `intervaltree` (Apache-2.0). Annotations whose reference is not a
`MediaRef` (i.e., they have no time interval) are kept in a separate
list — they can still be enumerated via `all()` / `by_tier` but
won’t appear in interval queries.

### Classes

| [`MemoryStore`](_autosummary/lacing.store.memory.html.md#lacing.store.memory.MemoryStore)()   | `IntervalAnnotationStore` implementation over `intervaltree`.   |
|------------------------------------------------------------------|-----------------------------------------------------------------|

### *class* lacing.store.memory.MemoryStore

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

`IntervalAnnotationStore` implementation over `intervaltree`.

Conforms to the protocol in `lacing.store.base`. We don’t formally
inherit from `IntervalAnnotationStore` because it’s a `Protocol`
with method bodies — structural typing is enough.

#### lock(key)

Exclusive lock named `key` among the threads of this process.

* **Return type:**
  [`AbstractContextManager`](https://docs.python.org/3/library/contextlib.html#contextlib.AbstractContextManager)[[`None`](https://docs.python.org/3/builtins/constants.html#None)]


# _autosummary/lacing.store.migrations.html.md

# lacing.store.migrations

Store-level schema migrations — the on-disk counterpart of the body ladder.

[`lacing.schema`](_autosummary/lacing.schema.html.md#module-lacing.schema) migrates annotation *bodies* (`dict -> dict`, keyed
`(schema_name, from_version)`). This module is the same mental model one
level down: it migrates the *store* — table layout, column rewrites, the
`meta.schema_version` stamp — keyed `(store_kind, from_version)`.

The split matters because the two ladders move different things:

- a body migration rewrites one annotation’s payload and can run anywhere;
- a store migration receives an **open connection** and performs DDL, row
  rewrites, and the version stamp for a whole database, atomically.

Contract (mirrors [`lacing.schema.register_migration()`](_autosummary/lacing.schema.html.md#lacing.schema.register_migration)):

- steps are single-step only (`to_version == from_version + 1`); chains
  compose by repeated lookup;
- re-registering a `(store_kind, from_version)` pair replaces the previous
  entry — convenient in tests, intentional for hot-reload;
- an upgrade function receives the open connection and must leave the store
  readable at `to_version`, \*\*including writing the new version into the
  `meta` table\*\*.

What the runner guarantees around each step (all verified *inside* the
step’s transaction, so any breach rolls the whole step back):

- the version is re-read **under the write lock** before the step runs — a
  concurrent migrator that already applied the step is detected and the
  step skipped, never double-applied (multi-process servers open the same
  `.annot` file);
- the step stamped the version it claims to reach;
- `PRAGMA foreign_key_check` is clean, and the `annotations_rtree`
  index agrees with the `annotations` table (see
  [`rebuild_annotations_rtree()`](_autosummary/lacing.store.migrations.html.md#lacing.store.migrations.rebuild_annotations_rtree)).

Rules for step authors (sqlite):

- \*\*Never call `conn.executescript`, `COMMIT` or `ROLLBACK``** inside a
  step — ``executescript` implicitly commits the wrapper’s transaction,
  destroying atomicity. The runner detects a step that ended its
  transaction and fails loudly.
- Foreign-key enforcement is pinned **OFF** during migration (sqlite cannot
  rebuild tables under FK enforcement); `PRAGMA foreign_key_check` before
  commit is the compensating guarantee.
- A table rebuild (`CREATE new` → copy → `DROP old` → `RENAME`) must
  **preserve rowids** — `INSERT INTO new (rowid, ...) SELECT rowid, ...
  FROM old` — because `annotations_rtree` keys on them; rebuild the
  index with [`rebuild_annotations_rtree()`](_autosummary/lacing.store.migrations.html.md#lacing.store.migrations.rebuild_annotations_rtree) afterwards.

Backends own their runners (transaction idiom differs per driver):
[`migrate_annot_file()`](_autosummary/lacing.store.migrations.html.md#lacing.store.migrations.migrate_annot_file) here for SQLite `.annot` files; a Postgres
runner joins it with the first registered `"postgres"` step. Migration is
**opt-in** — `SqliteStore(path, migrate=True)` or `lacing migrate <path>`
— because silently rewriting a file on open is worse than refusing
(lacing#15).

### Module Attributes

| [`SQLITE_KIND`](_autosummary/lacing.store.migrations.html.md#lacing.store.migrations.SQLITE_KIND)                      | `store_kind` of the SQLite / `.annot` backend.                  |
|-----------------------------------------------------------------------------------|-----------------------------------------------------------------|
| [`POSTGRES_KIND`](_autosummary/lacing.store.migrations.html.md#lacing.store.migrations.POSTGRES_KIND)                    | `store_kind` of the Postgres backend.                           |
| [`SQLITE_MIGRATION_BUSY_TIMEOUT_MS`](_autosummary/lacing.store.migrations.html.md#lacing.store.migrations.SQLITE_MIGRATION_BUSY_TIMEOUT_MS) | How long a migrating connection waits on another writer's lock. |

### Functions

| [`migrate_annot_file`](_autosummary/lacing.store.migrations.html.md#lacing.store.migrations.migrate_annot_file)(path, \*[, to_version])      | Migrate a `.annot` file in place, returning `(from, to)` versions.      |
|--------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`migrate_sqlite_connection`](_autosummary/lacing.store.migrations.html.md#lacing.store.migrations.migrate_sqlite_connection)(conn, \*, to_version) | Run the SQLite ladder on an already-open connection.                    |
| [`reachable_versions`](_autosummary/lacing.store.migrations.html.md#lacing.store.migrations.reachable_versions)(store_kind, from_version)    | Versions reachable from `from_version` by chaining registered steps.    |
| [`rebuild_annotations_rtree`](_autosummary/lacing.store.migrations.html.md#lacing.store.migrations.rebuild_annotations_rtree)(conn)                 | Rebuild the interval index from the `annotations` table, in place.      |
| [`register_store_migration`](_autosummary/lacing.store.migrations.html.md#lacing.store.migrations.register_store_migration)(\*, store_kind, ...)   | Register a forward store migration from `from_version` to `to_version`. |

### Exceptions

| [`StoreMigrationError`](_autosummary/lacing.store.migrations.html.md#lacing.store.migrations.StoreMigrationError)   | Raised when a store migration step is missing or fails.   |
|------------------------------------------------------------------------|-----------------------------------------------------------|

### lacing.store.migrations.POSTGRES_KIND *= 'postgres'*

`store_kind` of the Postgres backend.

### lacing.store.migrations.SQLITE_KIND *= 'sqlite'*

`store_kind` of the SQLite / `.annot` backend.

### lacing.store.migrations.SQLITE_MIGRATION_BUSY_TIMEOUT_MS *= 30000*

How long a migrating connection waits on another writer’s lock.

Generous on purpose: when several workers race to open the same `.annot`
with `migrate=True`, the losers should wait for the winner and then skip
the already-applied steps, not fail at sqlite’s 5s default.

### *exception* lacing.store.migrations.StoreMigrationError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised when a store migration step is missing or fails.

### lacing.store.migrations.migrate_annot_file(path, , to_version=None)

Migrate a `.annot` file in place, returning `(from, to)` versions.

`to_version` defaults to the current build’s
[`lacing.store.sqlite.SCHEMA_VERSION`](_autosummary/lacing.store.sqlite.html.md#lacing.store.sqlite.SCHEMA_VERSION). Already-current files are a
no-op (`from == to`). Each step runs in its own `BEGIN IMMEDIATE`
transaction with the version re-checked under the lock, so concurrent
migrators converge and an interrupted chain resumes from the last
version that completed (idempotent).

Raises [`StoreMigrationError`](_autosummary/lacing.store.migrations.html.md#lacing.store.migrations.StoreMigrationError) when the file does not exist, is
not a `.annot` file, a step is missing, fails, or breaks one of the
runner’s in-transaction guarantees.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`int`](https://docs.python.org/3/builtins/functions.html#int)]

### lacing.store.migrations.migrate_sqlite_connection(conn, , to_version)

Run the SQLite ladder on an already-open connection.

The hook [`SqliteStore`](_autosummary/lacing.store.sqlite.html.md#lacing.store.sqlite.SqliteStore) uses for its
`migrate=True` opt-in; external callers with a file path want
[`migrate_annot_file()`](_autosummary/lacing.store.migrations.html.md#lacing.store.migrations.migrate_annot_file).

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

### lacing.store.migrations.reachable_versions(store_kind, from_version)

Versions reachable from `from_version` by chaining registered steps.

Ascending, excluding `from_version` itself. Empty when no step leaves
`from_version` — which is what a refusal message should say out loud
instead of the bare “run a migration” it used to say.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### lacing.store.migrations.rebuild_annotations_rtree(conn)

Rebuild the interval index from the `annotations` table, in place.

For use *inside* a migration step after a table rebuild. Reproduces the
store’s ULP-widening contract (bounds widened by one float ULP so
float→exact-bound comparisons never drop hits — see
[`lacing.store.sqlite`](_autosummary/lacing.store.sqlite.html.md#module-lacing.store.sqlite)). Returns the number of rows indexed.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

### lacing.store.migrations.register_store_migration(, store_kind, from_version, to_version)

Register a forward store migration from `from_version` to `to_version`.

The decorated function takes the backend’s open connection and must
perform every change of the step — DDL, row rewrites, and the
`meta.schema_version` write. Steps must be one version at a time
(`to_version == from_version + 1`); the runner chains them.

Step authors: read the module docstring’s rules — no `executescript`
/ `COMMIT` / `ROLLBACK` inside a step, preserve rowids on table
rebuilds, and rebuild the interval index with
[`rebuild_annotations_rtree()`](_autosummary/lacing.store.migrations.html.md#lacing.store.migrations.rebuild_annotations_rtree) if the `annotations` table was
rebuilt.

Re-registering the same `(store_kind, from_version)` pair replaces the
previous entry.


# _autosummary/lacing.store.postgres.html.md

# lacing.store.postgres

PostgreSQL-backed annotation store using `int8range` + GiST.

Maps lacing’s interval model to Postgres native range types (BACK-DOC §4.2):

| Allen relation   | Postgres operator (int8range / tstzrange)                   |
|------------------|-------------------------------------------------------------|
| overlaps (any)   | `a && b`      — any kind of intersection                    |
| during           | `a <@ b`      — a contained in b (strict variant: see code) |
| contains         | `a @> b`      — a contains b (strict variant: see code)     |
| meets / met_by   | `a -|- b`     — adjacent (touching but not overlapping)     |
| equals           | `a = b`                                                     |
| before           | `a << b`                                                    |
| after            | `a >> b`                                                    |
| strict overlap   | `a && b AND lower(a) < lower(b) AND upper(a) < upper(b)`    |

We store intervals as `int8range(start_value, end_value, '[)')` — half-open,
matching lacing’s `TimeInterval` semantics — at a \*\*per-(owner, project)
rate\*\* recorded in the `projects` table. All inserts must be at that rate;
re-quantizing happens on insert if necessary.

GiST index on the range column gives sub-millisecond overlap queries at
million-row scale. Optional `EXCLUDE USING GIST` constraints are added
**per tier** for stereotypes like `TIME_SUBDIVISION` that forbid overlap.

## Multi-tenancy (Phase 4, reelee#177)

Multiple logical stores (one per nw/reelee project) coexist in a single
Postgres database via **tenant columns on shared tables** (the decision in
`reelee/docs/storage_migration_plan.md` §”Postgres tenancy”): every
`annotations` and `tiers` row carries an `owner_id` and a `project_id`,
and every query is scoped by `(owner_id, project_id)`. A `PostgresStore`
instance is bound to one `(owner_id, project_id)` at construction and behaves
exactly like a single-tenant store; two stores with different project ids over
the same database never see each other’s rows.

`owner_id` is a forward seam for the multi-tenant access layer (reelee#174):
the column is carried and scoped now, but its *enforcement* (the policy
decision point) is deferred — today every store defaults `owner_id="default"`.

The per-tier `EXCLUDE USING GIST` no-overlap constraint is likewise scoped by
`(owner_id, project_id, tier)` so one project’s non-overlap rule cannot block
another project’s annotations.

## Connection pooling (Phase 4)

`nw`’s `ProjectGraph` opens and closes the store per
operation. That is fine for a SQLite file but pathological for a network DB, so
this module keeps a process-wide `psycopg_pool.ConnectionPool` per
connection string (see [`get_pool()`](_autosummary/lacing.store.postgres.html.md#lacing.store.postgres.get_pool)); a `PostgresStore` borrows a
connection from the pool for its lifetime and returns it on `close()` instead
of opening a fresh TCP/auth round-trip each time. When `psycopg_pool` is not
installed (or a caller passes `use_pool=False`) we fall back to a dedicated
long-lived connection.

Schema versioning lives in `meta`; mismatched versions raise on open.

Status: Phase 4. Tested via `pytest-postgresql` sandbox (no live server
needed for development).

### Module Attributes

| [`SCHEMA_VERSION`](_autosummary/lacing.store.postgres.html.md#lacing.store.postgres.SCHEMA_VERSION)     | Current schema version stored in the `meta` table.                        |
|---------------------------------------------------------------------|---------------------------------------------------------------------------|
| [`DEFAULT_OWNER_ID`](_autosummary/lacing.store.postgres.html.md#lacing.store.postgres.DEFAULT_OWNER_ID)   | Owner placeholder until the multi-tenant access layer (reelee#174) lands. |
| [`DEFAULT_PROJECT_ID`](_autosummary/lacing.store.postgres.html.md#lacing.store.postgres.DEFAULT_PROJECT_ID) | Project placeholder for single-project / legacy use.                      |

### Functions

| [`close_all_pools`](_autosummary/lacing.store.postgres.html.md#lacing.store.postgres.close_all_pools)()                                | Close every registered connection pool (test teardown / shutdown).   |
|---------------------------------------------------------------------------------------------------|----------------------------------------------------------------------|
| [`from_memory`](_autosummary/lacing.store.postgres.html.md#lacing.store.postgres.from_memory)(memory_store, connection_string, \*) | Replicate an in-memory store into a Postgres database (one tenant).  |
| [`get_pool`](_autosummary/lacing.store.postgres.html.md#lacing.store.postgres.get_pool)(connection_string, \*\*pool_kwargs)     | Return a process-wide `psycopg_pool.ConnectionPool` for a conninfo.  |
| [`to_memory`](_autosummary/lacing.store.postgres.html.md#lacing.store.postgres.to_memory)(pg_store)                              | Snapshot a `PostgresStore` into a `MemoryStore`.                     |

### Classes

| [`PostgresStore`](_autosummary/lacing.store.postgres.html.md#lacing.store.postgres.PostgresStore)(connection_string, \*[, rate, ...])   | PostgreSQL-backed `IntervalAnnotationStore`, scoped to one tenant.   |
|------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------|

### Exceptions

| [`PgSchemaMismatchError`](_autosummary/lacing.store.postgres.html.md#lacing.store.postgres.PgSchemaMismatchError)   | Raised on opening a database whose schema_version differs.                      |
|--------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| [`RateMismatchError`](_autosummary/lacing.store.postgres.html.md#lacing.store.postgres.RateMismatchError)       | Raised when an annotation's interval cannot be represented at the project rate. |
| [`TierOverlapError`](_autosummary/lacing.store.postgres.html.md#lacing.store.postgres.TierOverlapError)        | Raised when an insert would violate a per-tier no-overlap constraint.           |

### lacing.store.postgres.DEFAULT_OWNER_ID *= 'default'*

Owner placeholder until the multi-tenant access layer (reelee#174) lands.

The `owner_id` column is carried and scoped now; its enforcement is deferred.

### lacing.store.postgres.DEFAULT_PROJECT_ID *= 'default'*

Project placeholder for single-project / legacy use.

### *exception* lacing.store.postgres.PgSchemaMismatchError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised on opening a database whose schema_version differs.

### *class* lacing.store.postgres.PostgresStore(connection_string, , rate=24000, owner_id='default', project_id='default', autocommit=True, use_pool=True)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

PostgreSQL-backed `IntervalAnnotationStore`, scoped to one tenant.

A store instance is bound to one `(owner_id, project_id)` pair; multiple
instances over the same database (different project ids) coexist without
seeing each other’s annotations or tiers (see the module docstring on
multi-tenancy). The mapping/Allen/tier surface is unchanged — the tenant
scoping is transparent.

* **Parameters:**
  * **connection_string** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)) – A psycopg-compatible conninfo URL, or a kwargs dict
    (`host`/`port`/`user`/`password`/`dbname`).
  * **rate** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Project-wide rate for *this* `(owner, project)`. Set on first
    init for the pair; re-opening the same pair with a different rate
    raises [`PgSchemaMismatchError`](_autosummary/lacing.store.postgres.html.md#lacing.store.postgres.PgSchemaMismatchError). Different projects in the
    same DB may have different rates.
  * **owner_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Tenant owner. Defaults to [`DEFAULT_OWNER_ID`](_autosummary/lacing.store.postgres.html.md#lacing.store.postgres.DEFAULT_OWNER_ID) — the
    forward seam for the access layer (reelee#174); enforcement deferred.
  * **project_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Logical project key. Defaults to [`DEFAULT_PROJECT_ID`](_autosummary/lacing.store.postgres.html.md#lacing.store.postgres.DEFAULT_PROJECT_ID).
    `nw` passes the project’s `project_asset_id` here.
  * **autocommit** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True (default), statements run in their own transaction.
  * **use_pool** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – When True (default) and the connection is given as a string,
    borrow a connection from a process-wide pool keyed by the conninfo
    (amortizes per-op connect cost; see [`get_pool()`](_autosummary/lacing.store.postgres.html.md#lacing.store.postgres.get_pool)). Falls back to
    a dedicated connection when `psycopg_pool` is unavailable, when the
    connection is given as a dict, or when `use_pool=False`.

#### add_tier(tier, , enforce_no_overlap=False)

Add or update a tier.

* **Parameters:**
  * **tier** ([`Tier`](_autosummary/lacing.tier.html.md#lacing.tier.Tier)) – Tier definition.
  * **enforce_no_overlap** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, install a per-tier `EXCLUDE USING
    GIST` constraint forbidding overlapping annotations within
    this tier. Required for proper TIME_SUBDIVISION enforcement.
    Cannot be toggled after the tier has annotations.
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### lock(key)

Exclusive lock named `key` for this `(owner, project)`, across hosts.

A session-level `pg_advisory_lock` keyed on a 64-bit hash of
`owner_id` + `project_id` + `key`, taken on a dedicated connection
(never the store’s own, which other threads and transactions share) and
released in `finally`. If the holder crashes, Postgres drops the lock
when its connection dies, so nothing can go stale. Blocks until
available; not re-entrant.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`None`](https://docs.python.org/3/builtins/constants.html#None)]

#### update(annotation)

Replace the row with this id in ONE `UPDATE` statement (lacing#66).

A single statement is atomic in any isolation mode, so a concurrent
reader sees the old row or the new one — unlike `remove` then `add`,
which commit twice. Raises `KeyError` if the id is not in this
`(owner, project)` and `TierOverlapError` like `add()`.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *exception* lacing.store.postgres.RateMismatchError

Bases: [`LossyTimeConversionError`](_autosummary/lacing.time.html.md#lacing.time.LossyTimeConversionError)

Raised when an annotation’s interval cannot be represented at the project rate.

### lacing.store.postgres.SCHEMA_VERSION *= 3*

Current schema version stored in the `meta` table.

v2 (Phase 4, reelee#177) added tenant columns (`owner_id` / `project_id`)
to `tiers` and `annotations`, and a per-`(owner, project)` `projects`
table holding the rate (was a single `meta` row in v1).

v3 (lacing#14, defect D5): `prov_was_derived_from` may contain 64-hex
artifact `asset_id` strings alongside annotation UUIDs. Layout and rows
unchanged — the bump makes pre-v3 builds (whose read path eagerly
`UUID()`-parses the column) refuse rather than crash mid-read.

The migration ladder ([`lacing.store.migrations`](_autosummary/lacing.store.migrations.html.md#module-lacing.store.migrations),
`store_kind="postgres"`) is deliberately **empty**: no Postgres database
at any version ever held real data — the backend is config-selectable and
was never enabled in any environment; production has always been the
per-caller sqlite `.annot` layout (verified 2026-08-15, lacing#15).
Dev databases at v1/v2 are recreated, not migrated. Register steps from
v3 forward once a deployed Postgres exists.

### *exception* lacing.store.postgres.TierOverlapError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised when an insert would violate a per-tier no-overlap constraint.

### lacing.store.postgres.close_all_pools()

Close every registered connection pool (test teardown / shutdown).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### lacing.store.postgres.from_memory(memory_store, connection_string, , rate=24000, owner_id='default', project_id='default')

Replicate an in-memory store into a Postgres database (one tenant).

* **Return type:**
  [`PostgresStore`](_autosummary/lacing.store.postgres.html.md#lacing.store.postgres.PostgresStore)

### lacing.store.postgres.get_pool(connection_string, \*\*pool_kwargs)

Return a process-wide `psycopg_pool.ConnectionPool` for a conninfo.

One pool per distinct `connection_string` (the keying happens here, not at
the call-site), created lazily on first request. Raises [`ImportError`](https://docs.python.org/3/builtins/exceptions.html#ImportError)
if `psycopg_pool` is not installed — callers that want a graceful fallback
catch it (see [`PostgresStore`](_autosummary/lacing.store.postgres.html.md#lacing.store.postgres.PostgresStore)).

* **Parameters:**
  * **connection_string** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – psycopg conninfo URL (the registry key).
  * **\*\*pool_kwargs** – forwarded to `ConnectionPool` on
    first creation (e.g. `min_size`, `max_size`); ignored on reuse.
* **Returns:**
  The shared pool for this conninfo.

### lacing.store.postgres.to_memory(pg_store)

Snapshot a `PostgresStore` into a `MemoryStore`.


# _autosummary/lacing.store.sqlite.html.md

# lacing.store.sqlite

SQLite-backed annotation store and the `.annot` portable file format.

A `SqliteStore` is both:

1. A persistent backend implementing the `IntervalAnnotationStore`
   protocol over a single SQLite file.
2. The on-disk shape of the `.annot` portable handoff format
   (BACK-DOC §3.1: “SQLite-as-app-format” — Git-trackable,
   email-attachable, single-file).

Design notes:

- The integer pair `(start_value, start_rate)` and `(end_value, end_rate)`
  are the **source of truth** for time. Rational arithmetic stays exact.
- The R\*Tree index uses `REAL` (float seconds) because SQLite’s R\*Tree
  requires it. We treat R\*Tree results as a **pre-filter** and re-check
  boundary cases against the integer-pair columns. Any annotation whose
  rational seconds matches the query’s bounds exactly will still be
  filtered correctly because we widen the R\*Tree query by one ULP.
- Tier hierarchy is enforced via FK; stereotype validation is the
  application’s job (see `lacing.tier.validate_tier_constraint`).
- `schema_version` lives in the `meta` table; migrations register
  upgrade functions keyed on the from-version in
  [`lacing.store.migrations`](_autosummary/lacing.store.migrations.html.md#module-lacing.store.migrations) (the store-level counterpart of the
  > body ladder in [`lacing.schema`](_autosummary/lacing.schema.html.md#module-lacing.schema)). Opening a stale file refuses
  > unless asked to migrate (`SqliteStore(path, migrate=True)` or
  > `lacing migrate <path>`), and refuses *before* touching the file.

See `lacing-architecture` (Phase 1) and BACK-DOC §3.1 for the why.

### Module Attributes

| [`SCHEMA_VERSION`](_autosummary/lacing.store.sqlite.html.md#lacing.store.sqlite.SCHEMA_VERSION)   | Current `.annot` schema version.   |
|-------------------------------------------------------------------|------------------------------------|

### Functions

| [`from_memory`](_autosummary/lacing.store.sqlite.html.md#lacing.store.sqlite.from_memory)(memory_store, target)   | Persist an in-memory store to a new `.annot` file.   |
|--------------------------------------------------------------------------------------|------------------------------------------------------|
| [`to_memory`](_autosummary/lacing.store.sqlite.html.md#lacing.store.sqlite.to_memory)(sqlite_store)             | Load a `.annot` file fully into a `MemoryStore`.     |

### Classes

| [`SqliteStore`](_autosummary/lacing.store.sqlite.html.md#lacing.store.sqlite.SqliteStore)(path, \*[, check_same_thread, ...])   | SQLite-backed `IntervalAnnotationStore`.   |
|----------------------------------------------------------------------------------------------------|--------------------------------------------|

### Exceptions

| [`SchemaMismatchError`](_autosummary/lacing.store.sqlite.html.md#lacing.store.sqlite.SchemaMismatchError)   | Raised when opening a `.annot` file with an incompatible schema.   |
|------------------------------------------------------------------------|--------------------------------------------------------------------|

### lacing.store.sqlite.SCHEMA_VERSION *= 2*

Current `.annot` schema version. Increment + register a migration
([`lacing.store.migrations.register_store_migration()`](_autosummary/lacing.store.migrations.html.md#lacing.store.migrations.register_store_migration), with
`store_kind="sqlite"`) when making a breaking change to the table
layout.

v2 (lacing#14, defect D5): `prov_was_derived_from` may contain 64-hex
artifact `asset_id` strings alongside annotation UUIDs. The table layout
and every existing row are unchanged — the bump exists because \*\*pre-v2
builds eagerly `UUID()`-parse the column on read\*\* and crash on the
first asset id, so they must refuse v2 files instead of opening them and
failing row-by-row. The v1→v2 migration step is accordingly stamp-only
(see [`lacing.store.migrations`](_autosummary/lacing.store.migrations.html.md#module-lacing.store.migrations)).

Operational caveat: the version gate runs at **open**. A pre-v2 build’s
already-open connection survives the stamp and keeps reading — safe until
some v2 writer mints an asset-id ref, at which point it fails as a raw
`ValueError` mid-read rather than a refusal. Restart long-lived
pre-v2 services after migrating a file they serve.

### *exception* lacing.store.sqlite.SchemaMismatchError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised when opening a `.annot` file with an incompatible schema.

### *class* lacing.store.sqlite.SqliteStore(path, , check_same_thread=True, migrate=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

SQLite-backed `IntervalAnnotationStore`.

* **Parameters:**
  * **path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike)) – Path to the `.annot` file. Use `":memory:"` for an
    ephemeral in-memory database.
  * **check_same_thread** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Forwarded to `sqlite3.connect`. We hold a
    single connection guarded by a lock; pass `False` when
    sharing across threads.
  * **migrate** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Opt-in to upgrading a file written at an older
    `schema_version` on open, via the ladder in
    [`lacing.store.migrations`](_autosummary/lacing.store.migrations.html.md#module-lacing.store.migrations). Off by default — silently
    rewriting someone’s file on open is worse than refusing.
    Every open-time schema failure — refusal *or* failed migration
    — raises [`SchemaMismatchError`](_autosummary/lacing.store.sqlite.html.md#lacing.store.sqlite.SchemaMismatchError); a failed migration
    chains the ladder’s `StoreMigrationError` as its cause.

#### lock(key)

Exclusive lock named `key`: a file lock beside the database.

Excludes other threads and other processes on this host that open the
same `.annot` file; the kernel frees it if the holder dies. An
in-memory database (`":memory:"`) has no file, so it falls back to a
process-local lock.

* **Return type:**
  [`AbstractContextManager`](https://docs.python.org/3/library/contextlib.html#contextlib.AbstractContextManager)[[`None`](https://docs.python.org/3/builtins/constants.html#None)]

#### update(annotation)

Replace the row with this id (and its R\*Tree entry) in one transaction.

Readers take the same lock (`_fetchall()`), so on a connection
shared between threads — where a reader would otherwise see this
transaction’s uncommitted state — they get the old annotation or the
new one, never neither (lacing#66).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### lacing.store.sqlite.from_memory(memory_store, target)

Persist an in-memory store to a new `.annot` file.

* **Return type:**
  [`SqliteStore`](_autosummary/lacing.store.sqlite.html.md#lacing.store.sqlite.SqliteStore)

### lacing.store.sqlite.to_memory(sqlite_store)

Load a `.annot` file fully into a `MemoryStore`.


# _autosummary/lacing.tier.html.md

# lacing.tier

Tiers and the five ELAN stereotypes.

A tier is a named layer of annotations. Stereotypes constrain how a tier
relates to its parent. We adopt ELAN’s five stereotypes verbatim — see
ANN-DOC §C and OSS-DOC tier-2.4.

### Functions

| [`validate_tier_constraint`](_autosummary/lacing.tier.html.md#lacing.tier.validate_tier_constraint)(parent, child, ...)   | Check that `child_intervals` satisfy `child.stereotype` against `parent_intervals`.   |
|-------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------|

### Classes

| [`Tier`](_autosummary/lacing.tier.html.md#lacing.tier.Tier)(name, \*[, stereotype, parent, metadata])   | A named annotation layer with optional parent and stereotype.   |
|---------------------------------------------------------------------------------------------------|-----------------------------------------------------------------|
| [`TierStereotype`](_autosummary/lacing.tier.html.md#lacing.tier.TierStereotype)(\*values)                         | Constraints on how a child tier relates to its parent tier.     |

### *class* lacing.tier.Tier(name, , stereotype=TierStereotype.NONE, parent=None, metadata=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A named annotation layer with optional parent and stereotype.

Tiers are pure metadata; they don’t own annotations. The store is keyed
by interval, not by tier — annotations carry their tier name as a field.
This matches ELAN’s TIME_ORDER indirection (see ANN-DOC §C).

### *class* lacing.tier.TierStereotype(\*values)

Bases: [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Enum`](https://docs.python.org/3/library/enum.html#enum.Enum)

Constraints on how a child tier relates to its parent tier.

Names match ELAN exactly so EAF round-trips are trivial.

#### INCLUDED_IN *= 'INCLUDED_IN'*

Children lie within the parent but gaps between siblings are allowed.

#### NONE *= 'NONE'*

No parent constraint. Top-level tier.

#### SYMBOLIC_ASSOCIATION *= 'SYMBOLIC_ASSOCIATION'*

One-to-one association with parent; child shares parent’s interval exactly.

#### SYMBOLIC_SUBDIVISION *= 'SYMBOLIC_SUBDIVISION'*

Ordered subdivision; children share parent’s interval as a sequence (no times).

#### TIME_SUBDIVISION *= 'TIME_SUBDIVISION'*

Children fully partition the parent’s interval (no gaps, no overlap).

### lacing.tier.validate_tier_constraint(parent, child, parent_intervals, child_intervals)

Check that `child_intervals` satisfy `child.stereotype` against `parent_intervals`.

Returns a list of violation messages (empty list = valid). Pure function;
callers decide whether to raise, log, or surface in UI.

Lives here rather than on `Tier` so the constraint logic is testable
in isolation and reusable from server validators and UI plugins alike.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]


# _autosummary/lacing.time.html.md

# lacing.time

Rational time and half-open intervals — the foundations.

Time in lacing is always rational, never float. See
`.claude/skills/lacing-time-and-intervals/SKILL.md` and BACK-DOC §2.1.

Wire format: `{"v": int, "r": int}`. Python uses `fractions.Fraction`
for arithmetic. `to_seconds()` is a one-way escape hatch for display
and third-party libs that demand a float.

### Module Attributes

| [`DEFAULT_RATE`](_autosummary/lacing.time.html.md#lacing.time.DEFAULT_RATE)              | Default rate (ticks/second).                                                |
|----------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| [`RATIONAL_TIME_JSON_SCHEMA`](_autosummary/lacing.time.html.md#lacing.time.RATIONAL_TIME_JSON_SCHEMA) | JSON Schema for the `RationalTime` wire shape `{"v": int, "r": int}`.       |
| [`TIME_INTERVAL_JSON_SCHEMA`](_autosummary/lacing.time.html.md#lacing.time.TIME_INTERVAL_JSON_SCHEMA) | JSON Schema for the `TimeInterval` wire shape `{"start": ..., "end": ...}`. |

### Classes

| [`RationalTime`](_autosummary/lacing.time.html.md#lacing.time.RationalTime)(value[, rate])   | A point in time as `value / rate` seconds.   |
|--------------------------------------------------------------------------------|----------------------------------------------|
| [`TimeInterval`](_autosummary/lacing.time.html.md#lacing.time.TimeInterval)(start, end)      | A half-open interval `[start, end)`.         |

### Exceptions

| [`LossyTimeConversionError`](_autosummary/lacing.time.html.md#lacing.time.LossyTimeConversionError)   | Raised when a rate or seconds conversion would lose precision.   |
|-----------------------------------------------------------------------------|------------------------------------------------------------------|

### lacing.time.DEFAULT_RATE *: [int](https://docs.python.org/3/builtins/functions.html#int)* *= 24000*

Default rate (ticks/second). LCM 1008000 covers all common video rates exactly.

### *exception* lacing.time.LossyTimeConversionError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

Raised when a rate or seconds conversion would lose precision.

### lacing.time.RATIONAL_TIME_JSON_SCHEMA *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]* *= {'additionalProperties': False, 'description': 'A point in time as v/r seconds. Both members are integers — never a float, never a JSON number with a fractional part.', 'properties': {'r': {'description': 'Rate in ticks per second. Strictly positive.', 'exclusiveMinimum': 0, 'type': 'integer'}, 'v': {'description': 'Tick count (may be negative).', 'type': 'integer'}}, 'required': ['v', 'r'], 'title': 'RationalTime', 'type': 'object'}*

JSON Schema for the `RationalTime` wire shape `{"v": int, "r": int}`.

Non-negotiable 1 lives here as much as in the constructor: `integer`, not
`number`. A JSON Schema validator must reject `{"v": 0.5, "r": 48000}`.

### *class* lacing.time.RationalTime(value, rate=24000)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A point in time as `value / rate` seconds.

Immutable. Two `RationalTime` values with different rates compare via
their rational value, so `RationalTime(24, 24) == RationalTime(1, 1)`.

### Examples

```pycon
>>> RationalTime(24000) == RationalTime(1, 1)
True
>>> RationalTime.from_seconds("1.5", rate=2).value
3
```

#### *classmethod* from_seconds(seconds, rate=24000)

Build from seconds. Quantizes to `rate`; raises if lossy.

`seconds` may be a `str` like `"1.001"` to avoid float ingestion.

* **Return type:**
  [`RationalTime`](_autosummary/lacing.time.html.md#lacing.time.RationalTime)

#### *classmethod* from_seconds_lossy(seconds, , rate=24000, mode='round')

Build from seconds, quantizing to the nearest sample at `rate`.

Unlike [`from_seconds()`](_autosummary/lacing.time.html.md#lacing.time.RationalTime.from_seconds) — which raises
[`LossyTimeConversionError`](_autosummary/lacing.time.html.md#lacing.time.LossyTimeConversionError) when the value cannot be
represented exactly — this method always succeeds by quantizing.
`mode` selects the rounding rule:

- `"round"` — nearest sample, ties to even (default)
- `"floor"` — largest sample <= `seconds`
- `"ceil"`  — smallest sample >= `seconds`

Use this when sample-level quantization is knowingly acceptable —
the common case for user-supplied durations. Use [`from_seconds()`](_autosummary/lacing.time.html.md#lacing.time.RationalTime.from_seconds)
when exactness matters and a lossy conversion should be an error.

### Examples

```pycon
>>> RationalTime.from_seconds_lossy("0.1", rate=3).value
0
>>> RationalTime.from_seconds_lossy("0.1", rate=3, mode="ceil").value
1
```

* **Return type:**
  [`RationalTime`](_autosummary/lacing.time.html.md#lacing.time.RationalTime)

#### *classmethod* from_wire(d)

Build from the wire form. Unknown keys are an error, not ignored.

`RATIONAL_TIME_JSON_SCHEMA` sets `additionalProperties: false` and
lacing-ui’s Zod mirror is `.strict()`; accepting extras here would
make Python the one lax end of a contract both other ends enforce, so
a `{"v": 0, "r": 1, "seconds": 0.0}` payload would round-trip through
Python and then be rejected by the frontend.

* **Return type:**
  [`RationalTime`](_autosummary/lacing.time.html.md#lacing.time.RationalTime)

#### *classmethod* now(rate=24000)

Wall-clock time as a [`RationalTime`](_autosummary/lacing.time.html.md#lacing.time.RationalTime), quantized to `rate`.

Uses `time.time_ns()` and builds the value directly, sidestepping
the float-quantization landmine of `from_seconds(float)`. Every
producer of an annotation or artifact needs this for
`Provenance.generated_at_time` — the only value that field reads
as a *known* time. Tick 0 there is the UNKNOWN sentinel
(`lacing.model.UNKNOWN_GENERATED_AT`), never the epoch.

* **Return type:**
  [`RationalTime`](_autosummary/lacing.time.html.md#lacing.time.RationalTime)

#### to_rate(new_rate)

Re-express at `new_rate`. Raises `LossyTimeConversionError` on loss.

* **Return type:**
  [`RationalTime`](_autosummary/lacing.time.html.md#lacing.time.RationalTime)

#### to_seconds()

Float seconds — for display only. Never round-trip through this.

* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

#### *classmethod* zero(rate=24000)

Tick 0 at `rate` — the start of a media timeline.

Not a wall-clock timestamp: as a `Provenance.generated_at_time` it
is the UNKNOWN sentinel (`lacing.model.UNKNOWN_GENERATED_AT`), not
the epoch. Producers stamp [`now()`](_autosummary/lacing.time.html.md#lacing.time.RationalTime.now) there.

* **Return type:**
  [`RationalTime`](_autosummary/lacing.time.html.md#lacing.time.RationalTime)

### lacing.time.TIME_INTERVAL_JSON_SCHEMA *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]* *= {'additionalProperties': False, 'description': 'A half-open interval [start, end). start == end is a point.', 'properties': {'end': {'additionalProperties': False, 'description': 'A point in time as v/r seconds. Both members are integers — never a float, never a JSON number with a fractional part.', 'properties': {'r': {'description': 'Rate in ticks per second. Strictly positive.', 'exclusiveMinimum': 0, 'type': 'integer'}, 'v': {'description': 'Tick count (may be negative).', 'type': 'integer'}}, 'required': ['v', 'r'], 'title': 'RationalTime', 'type': 'object'}, 'start': {'additionalProperties': False, 'description': 'A point in time as v/r seconds. Both members are integers — never a float, never a JSON number with a fractional part.', 'properties': {'r': {'description': 'Rate in ticks per second. Strictly positive.', 'exclusiveMinimum': 0, 'type': 'integer'}, 'v': {'description': 'Tick count (may be negative).', 'type': 'integer'}}, 'required': ['v', 'r'], 'title': 'RationalTime', 'type': 'object'}}, 'required': ['start', 'end'], 'title': 'TimeInterval', 'type': 'object'}*

JSON Schema for the `TimeInterval` wire shape `{"start": ..., "end": ...}`.

`start <= end` is a constructor invariant that JSON Schema cannot express;
Pydantic validation still enforces it.

### *class* lacing.time.TimeInterval(start, end)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A half-open interval `[start, end)`.

`start == end` is a valid point annotation, not a degenerate case.
Always `start <= end`; constructor raises `ValueError` otherwise.

#### *property* duration *: [RationalTime](_autosummary/lacing.time.html.md#lacing.time.RationalTime)*

`end - start` at the same rate as `start`.

#### *classmethod* from_wire(d)

Build from the wire form. See [`RationalTime.from_wire()`](_autosummary/lacing.time.html.md#lacing.time.RationalTime.from_wire) on extras.

* **Return type:**
  [`TimeInterval`](_autosummary/lacing.time.html.md#lacing.time.TimeInterval)


# _autosummary/lacing.tracks.html.md

# lacing.tracks

High-level “track-shaped” facades on top of `lacing`.

A `track` is an opinionated bundle of tiers that shows up in the wild
together. `subtitle` is the canonical example: `sections`,
`lines`, `words` over one audio asset, with a builder that hides
the `Annotation` / `MediaRef` / `Provenance` plumbing and a
query layer that takes `float` seconds instead of `RationalTime`.

These facades sit *on top of* the core store/tier/annotation API —
they don’t introduce new storage. Anything you can build with a
track facade can be built with the raw API; the facade just makes
the common case ergonomic and consistent.

### Functions

| [`register_subtitle_schemas`](_autosummary/lacing.tracks.html.md#lacing.tracks.register_subtitle_schemas)()   | Register permissive body validators for the subtitle URIs.   |
|--------------------------------------------------------------------------------|--------------------------------------------------------------|

### Classes

| [`SubtitleBuilder`](_autosummary/lacing.tracks.html.md#lacing.tracks.SubtitleBuilder)(store, \*, asset_id[, rate, ...])   | Builder for `(sections, lines, words)` tiers over one asset.   |
|------------------------------------------------------------------------------------------------------|----------------------------------------------------------------|
| [`SubtitleTrack`](_autosummary/lacing.tracks.html.md#lacing.tracks.SubtitleTrack)(store, \*, asset_id[, rate])          | Read-side facade for `(sections, lines, words)` over an asset. |

### *class* lacing.tracks.SubtitleBuilder(store, , asset_id, rate=1000, was_generated_by='lacing.tracks.subtitle', was_attributed_to='lacing', ensure_tiers=True)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Builder for `(sections, lines, words)` tiers over one asset.

The builder is a context manager *only as a convenience*; it has no
transactional semantics. Calling `.section()` / `.line()` /
`.word()` writes immediately, in the order given.

* **Parameters:**
  * **store** ([`IntervalAnnotationStore`](_autosummary/lacing.store.base.html.md#lacing.store.base.IntervalAnnotationStore)) – Any `IntervalAnnotationStore` (memory, sqlite, postgres).
  * **asset_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The `MediaRef.asset_id` to attach every annotation
    to. Typically a path or content-hash of the audio file.
  * **rate** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Tick rate for time intervals. Defaults to 1000 (ms).
  * **was_generated_by** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – `Provenance.was_generated_by`. Defaults to
    `"lacing.tracks.subtitle"`.
  * **was_attributed_to** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – `Provenance.was_attributed_to`.
  * **ensure_tiers** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – When True (default), creates the three tiers if
    they don’t exist. Pass False if your store uses different
    tier names and you’ve created them yourself.

#### add(, tier, body_schema_uri, body, start_s, end_s)

Add an annotation to `tier` with explicit schema/body.

* **Return type:**
  `_BuiltAnnotation`

#### line(text, start_s, end_s, , line_index=None, section='', words=(), extra=None)

Add a lyric line. `words` is an optional list of
`(text, start, end)` or `(text, start, end, confidence)`
tuples that will be inserted on the `words` tier.

* **Return type:**
  `_BuiltAnnotation`

#### section(label, start_s, end_s, , title='', energy='', mood='', extra=None)

Add a song section.

* **Return type:**
  `_BuiltAnnotation`

#### word(text, start_s, end_s, , line_index=None, confidence=None, extra=None)

Add one word.

* **Return type:**
  `_BuiltAnnotation`

### *class* lacing.tracks.SubtitleTrack(store, , asset_id, rate=1000)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Read-side facade for `(sections, lines, words)` over an asset.

Methods take `float` seconds; conversions to `RationalTime` are
handled internally. Returned annotations are sorted by start time.

* **Parameters:**
  * **store** ([`IntervalAnnotationStore`](_autosummary/lacing.store.base.html.md#lacing.store.base.IntervalAnnotationStore)) – Any `IntervalAnnotationStore`.
  * **asset_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – When set, query results are filtered to annotations
    whose `MediaRef.asset_id` equals this value. Pass
    `None` to ignore asset filtering (rarely what you want).
  * **rate** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Tick rate used to build query intervals.

#### lines_in(start_s, end_s)

Lines that overlap `[start_s, end_s]`.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### sections_covering(t)

Sections whose interval contains `t` (typically one).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### words_in(start_s, end_s)

Words that overlap `[start_s, end_s]`.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

### lacing.tracks.register_subtitle_schemas()

Register permissive body validators for the subtitle URIs.

Calling this is *optional*; the builder works fine without
registered schemas. Use it when you want body-shape errors to
surface at write time rather than later.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### Modules

| [`subtitle`](_autosummary/lacing.tracks.subtitle.html.md#module-lacing.tracks.subtitle)   | Subtitle / lyric / caption tracks — the (sections, lines, words) trio.   |
|-------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|


# _autosummary/lacing.tracks.subtitle.html.md

# lacing.tracks.subtitle

Subtitle / lyric / caption tracks — the (sections, lines, words) trio.

`SubtitleBuilder` lets callers add sections, lines, and words to a
store with float seconds and a single `asset_id`, instead of
hand-rolling `Annotation` / `MediaRef` / `Provenance` /
`TimeInterval`. `SubtitleTrack` provides matching read-side
convenience: `lines_in(window)`, `words_in(window)`,
`sections_covering(t)`.

Body schemas are conventional URIs:

- `annot://schema/song-section/v1` — `{label, title?, energy?, mood?}`
- `annot://schema/lyric-line/v1` — `{text, line_index?, section?}`
- `annot://schema/word/v1` — `{text, line_index?, confidence?}`
  : (matches the built-in [`lacing.bodies.word`](_autosummary/lacing.bodies.word.html.md#module-lacing.bodies.word) schema)

These are not enforced unless the caller has registered Pydantic
validators for them; `register_subtitle_schemas` will register simple
permissive validators if you want body validation. Otherwise the body
is just a dict.

Quick start:

```default
from lacing import MemoryStore
from lacing.tracks.subtitle import SubtitleBuilder, SubtitleTrack

store = MemoryStore()
with SubtitleBuilder(store, asset_id="song/audio.mp3") as b:
    b.section("intro", 0.0, 12.5)
    b.line("I came down to the river", 12.5, 16.2,
           section="verse_1", words=[
               ("I",     12.5, 12.7),
               ("came",  12.7, 13.0),
               ("down",  13.0, 13.3),
           ])

track = SubtitleTrack(store, asset_id="song/audio.mp3")
chorus_lines = track.lines_in(35.0, 55.0)
section_at_42 = track.sections_covering(42.0)
```

### Module Attributes

| [`BUILT_IN_BODY_SCHEMAS`](_autosummary/lacing.tracks.subtitle.html.md#lacing.tracks.subtitle.BUILT_IN_BODY_SCHEMAS)   | Mapping of body URI → minimal example body.   |
|--------------------------------------------------------------------------|-----------------------------------------------|

### Functions

| [`register_subtitle_schemas`](_autosummary/lacing.tracks.subtitle.html.md#lacing.tracks.subtitle.register_subtitle_schemas)()   | Register permissive body validators for the subtitle URIs.   |
|--------------------------------------------------------------------------------|--------------------------------------------------------------|

### Classes

| [`SubtitleBuilder`](_autosummary/lacing.tracks.subtitle.html.md#lacing.tracks.subtitle.SubtitleBuilder)(store, \*, asset_id[, rate, ...])   | Builder for `(sections, lines, words)` tiers over one asset.   |
|------------------------------------------------------------------------------------------------------|----------------------------------------------------------------|
| [`SubtitleTrack`](_autosummary/lacing.tracks.subtitle.html.md#lacing.tracks.subtitle.SubtitleTrack)(store, \*, asset_id[, rate])          | Read-side facade for `(sections, lines, words)` over an asset. |

### lacing.tracks.subtitle.BUILT_IN_BODY_SCHEMAS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]]* *= {'annot://schema/lyric-line/v1': {'line_index': 0, 'section': 'verse_1', 'text': 'I came down to the river'}, 'annot://schema/song-section/v1': {'energy': 'medium', 'label': 'verse_1', 'mood': 'wistful', 'title': 'verse 1'}, 'annot://schema/word/v1': {'confidence': 0.94, 'line_index': 0, 'text': 'river'}}*

Mapping of body URI → minimal example body. Used by
[`register_subtitle_schemas()`](_autosummary/lacing.tracks.subtitle.html.md#lacing.tracks.subtitle.register_subtitle_schemas) and as inline reference for
callers building outside the builder.

### *class* lacing.tracks.subtitle.SubtitleBuilder(store, , asset_id, rate=1000, was_generated_by='lacing.tracks.subtitle', was_attributed_to='lacing', ensure_tiers=True)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Builder for `(sections, lines, words)` tiers over one asset.

The builder is a context manager *only as a convenience*; it has no
transactional semantics. Calling `.section()` / `.line()` /
`.word()` writes immediately, in the order given.

* **Parameters:**
  * **store** ([`IntervalAnnotationStore`](_autosummary/lacing.store.base.html.md#lacing.store.base.IntervalAnnotationStore)) – Any `IntervalAnnotationStore` (memory, sqlite, postgres).
  * **asset_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The `MediaRef.asset_id` to attach every annotation
    to. Typically a path or content-hash of the audio file.
  * **rate** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Tick rate for time intervals. Defaults to 1000 (ms).
  * **was_generated_by** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – `Provenance.was_generated_by`. Defaults to
    `"lacing.tracks.subtitle"`.
  * **was_attributed_to** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – `Provenance.was_attributed_to`.
  * **ensure_tiers** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – When True (default), creates the three tiers if
    they don’t exist. Pass False if your store uses different
    tier names and you’ve created them yourself.

#### add(, tier, body_schema_uri, body, start_s, end_s)

Add an annotation to `tier` with explicit schema/body.

* **Return type:**
  `_BuiltAnnotation`

#### line(text, start_s, end_s, , line_index=None, section='', words=(), extra=None)

Add a lyric line. `words` is an optional list of
`(text, start, end)` or `(text, start, end, confidence)`
tuples that will be inserted on the `words` tier.

* **Return type:**
  `_BuiltAnnotation`

#### section(label, start_s, end_s, , title='', energy='', mood='', extra=None)

Add a song section.

* **Return type:**
  `_BuiltAnnotation`

#### word(text, start_s, end_s, , line_index=None, confidence=None, extra=None)

Add one word.

* **Return type:**
  `_BuiltAnnotation`

### *class* lacing.tracks.subtitle.SubtitleTrack(store, , asset_id, rate=1000)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Read-side facade for `(sections, lines, words)` over an asset.

Methods take `float` seconds; conversions to `RationalTime` are
handled internally. Returned annotations are sorted by start time.

* **Parameters:**
  * **store** ([`IntervalAnnotationStore`](_autosummary/lacing.store.base.html.md#lacing.store.base.IntervalAnnotationStore)) – Any `IntervalAnnotationStore`.
  * **asset_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – When set, query results are filtered to annotations
    whose `MediaRef.asset_id` equals this value. Pass
    `None` to ignore asset filtering (rarely what you want).
  * **rate** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Tick rate used to build query intervals.

#### lines_in(start_s, end_s)

Lines that overlap `[start_s, end_s]`.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### sections_covering(t)

Sections whose interval contains `t` (typically one).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

#### words_in(start_s, end_s)

Words that overlap `[start_s, end_s]`.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Annotation`](_autosummary/lacing.model.html.md#lacing.model.Annotation)]

### lacing.tracks.subtitle.register_subtitle_schemas()

Register permissive body validators for the subtitle URIs.

Calling this is *optional*; the builder works fine without
registered schemas. Use it when you want body-shape errors to
surface at write time rather than later.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/lacing.worker.html.md

# lacing.worker

Optional Arq integration — queue processors through Redis.

This module imports `arq` only when needed, so it’s safe to import
even without `[arq]` installed. The actual `WorkerSettings` class is
constructed lazily by [`build_worker_settings()`](_autosummary/lacing.worker.html.md#lacing.worker.build_worker_settings).

Usage:

```default
# In a worker process — assumes Redis is running.
import asyncio
from lacing.worker import build_worker_settings
from arq import run_worker

settings = build_worker_settings(
    store_factory=lambda: SqliteStore("project.annot", check_same_thread=False),
    oplog_factory=lambda: SqliteOpLog("project.annot.oplog", check_same_thread=False),
)
run_worker(settings)

# In your app — enqueue a processor:
from arq.connections import create_pool, RedisSettings

redis = await create_pool(RedisSettings())
await redis.enqueue_job(
    "run_processor",
    "low_confidence_review",
    threshold=0.4,
)
```

For testing and small-scale work, prefer [`lacing.processors.run_sync()`](_autosummary/lacing.processors.html.md#lacing.processors.run_sync)
— it runs the same registered processor in the current event loop with
no Redis needed.

### Functions

| [`build_worker_settings`](_autosummary/lacing.worker.html.md#lacing.worker.build_worker_settings)(\*, store_factory, ...)   | Build an Arq `WorkerSettings` class with lacing processors registered.   |
|--------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|

### lacing.worker.build_worker_settings(, store_factory, oplog_factory, redis_settings=None)

Build an Arq `WorkerSettings` class with lacing processors registered.

* **Parameters:**
  * **store_factory** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – Zero-arg callable returning a fresh
    `IntervalAnnotationStore` (per-job; processors mutate it).
  * **oplog_factory** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – Zero-arg callable returning a fresh `OpLog`.
  * **redis_settings** ([`Any`](https://docs.python.org/3/library/typing.html#typing.Any) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Optional `arq.connections.RedisSettings`.
* **Return type:**
  [`type`](https://docs.python.org/3/builtins/functions.html#type)
* **Returns:**
  A class suitable for `arq.run_worker(...)`.


# about-this-build.html.md

<!-- generated by epythet -->

# About this build

This documentation was built on **2026-10-14 13:03 UTC** from commit <a href="https://github.com/reeleehq/lacing/commit/a43715b686362004f2a0f007c08ec22bb5d281a9"><code>a43715b</code></a> on branch <code>main</code>, for **lacing 0.0.54** (from <code>pyproject.toml</code>).

#### NOTE
Nothing suggests a mismatch: the tree was clean at the commit above, and the documented version is the one on PyPI.

## Source

|                     |                                                                                                                                                        |
|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
| Commit              | <a href="https://github.com/reeleehq/lacing/commit/a43715b686362004f2a0f007c08ec22bb5d281a9"><code>a43715b686362004f2a0f007c08ec22bb5d281a9</code></a> |
| Branch              | <code>main</code>                                                                                                                                      |
| Tags at this commit | none                                                                                                                                                   |
| Working tree        | clean                                                                                                                                                  |
| Remote              | <code>https://github.com/reeleehq/lacing</code>                                                                                                        |

## Continuous integration

|              |                                                                                            |
|--------------|--------------------------------------------------------------------------------------------|
| Repository   | <code>reeleehq/lacing</code>                                                               |
| Run          | <a href="https://github.com/reeleehq/lacing/actions/runs/38568271096">38568271096</a>      |
| Ref          | <code>refs/heads/main</code>                                                               |
| Event commit | <code>1eddbb5ba18a68cec02e4f960ac75204794aa1a1</code> (in the history of the built commit) |

## Tools

|          |         |
|----------|---------|
| epythet  | 0.2.17  |
| Sphinx   | 9.1.0   |
| docutils | 0.22.4  |
| Python   | 3.12.15 |

## Configuration as resolved

|               |                                                                   |
|---------------|-------------------------------------------------------------------|
| theme         | <code>auto</code> (Sphinx theme <code>pydata_sphinx_theme</code>) |
| accent        | <code>#006e59</code>                                              |
| api_generator | <code>autosummary</code>                                          |
| ignore        | <code>tests/</code>, <code>scrap/</code>, <code>examples/</code>  |
| agent_outputs | <code>true</code>                                                 |
| aggregates    | <code>md</code>                                                   |
| ai_artifacts  | <code>true</code>                                                 |

## Package on PyPI

Latest release: <a href="https://pypi.org/project/lacing/0.0.54/">0.0.54</a>, the same as the documented version.

## Reproduce

```bash
git clone https://github.com/reeleehq/lacing && cd lacing
git checkout a43715b686362004f2a0f007c08ec22bb5d281a9
pip install "epythet==0.2.17"
epythet quickstart . --ignore tests/ scrap/ examples/
```

The same data, for machines: <a href="build_info.json"><code>build_info.json</code></a> (schema version 1).


# ai-agents.html.md

<!-- generated by epythet -->

# For AI agents

`lacing` ships artifacts for coding agents alongside its code. This page lists
them, says where each lives in the repository, and points at the
machine-readable copies of this documentation.

## Skills

Skills are folders holding a `SKILL.md` (the [Agent Skills](https://agentskills.io) format): a description that tells an agent when to use it and a body with the procedure. Install one into your agent with `gh skill` (any host: `--agent claude-code`, `copilot`, `cursor`, `codex`, `gemini`), or use the copy bundled in the wheel.

### `lacing-adapter-authoring`

Use when adding, modifying, or reviewing an I/O format adapter for lacing — Praat TextGrid, ELAN EAF, WebVTT, JAMS, Label Studio JSON, W3C Web Annotation, OpenTimelineIO, CoNLL, brat standoff, SubRip, TTML, CSV, or any new format. Triggers on “add a … adapter”, “import/export … format”, “support … in lacing”, “round-trip … format”, or any work under lacing/adapters/. Encodes the plugin contract, the round-trip test pattern, license-checking the underlying parser, schema URI conventions, and the common pitfalls (offset invalidation, rate mismatch, lossy round-trips).

Source: [`.claude/skills/lacing-adapter-authoring`](https://github.com/thorwhalen/lacing/tree/HEAD/.claude/skills/lacing-adapter-authoring).

### `lacing-architecture`

Use when starting any non-trivial work on the lacing package — adding modules, designing APIs, choosing dependencies, planning a new feature, or making architectural decisions. Triggers on tasks like “implement X in lacing”, “design the Y component”, “add support for Z format”, “where should this code go”, or any edit under lacing/ or lacing-ui/. Loads the ten non-negotiables, the package decomposition, and pointers to the design docs.

Source: [`.claude/skills/lacing-architecture`](https://github.com/thorwhalen/lacing/tree/HEAD/.claude/skills/lacing-architecture).

### `lacing-schema-codegen`

Use when modifying lacing’s data model, body schemas, or the Pydantic→JSON-Schema→Zod codegen pipeline. Triggers on edits to lacing/model.py, lacing/schema.py, lacing/tier.py, body_schema_uri, schema migrations, schema versioning, or anything in lacing-ui/src/types/generated/ or src/domain/envelope.ts that mirrors a Python type. Encodes the single-source-of-truth rule (Pydantic v2 is SoT), the additive-by-default versioning rule, the migration-registration pattern, and the codegen wiring (lacing-ui’s `npm run codegen` via scripts/codegen.mjs, CI-freshness-gated).

Source: [`.claude/skills/lacing-schema-codegen`](https://github.com/thorwhalen/lacing/tree/HEAD/.claude/skills/lacing-schema-codegen).

### `lacing-time-and-intervals`

Use when writing or reviewing any code in the lacing project that touches time, intervals, durations, rates, frame numbers, sample positions, timestamps, or interval queries. Triggers on `RationalTime`, `TimeInterval`, `fractions.Fraction`, `intervaltree`, Allen relations (overlap/during/meets/before), interval boundaries, half-open ranges, snapping, or any conversion between seconds/ticks/frames/samples. Catches the most common landmines: float drift, closed-vs-open boundaries, mixed rates, and ad-hoc overlap predicates.

Source: [`.claude/skills/lacing-time-and-intervals`](https://github.com/thorwhalen/lacing/tree/HEAD/.claude/skills/lacing-time-and-intervals).

## Instruction files

Files agents read before working in this repository.

- [`CLAUDE.md`](https://github.com/thorwhalen/lacing/tree/HEAD/CLAUDE.md): read by Claude Code

## Machine-readable documentation

This site publishes the same documentation in forms that fit an agent’s context window:

- [`llms.txt`](https://thorwhalen.github.io/lacing/llms.txt): an index of every page with a one-line description ([llms.txt](https://llmstxt.org) format)
- [`lacing.md`](https://thorwhalen.github.io/lacing/lacing.md): the whole documentation as one Markdown file
- `<page>.html.md`: a rendered Markdown twin of every page, advertised from each page’s `<head>` with `<link rel="alternate" type="text/markdown">`
- [`objects.inv`](https://thorwhalen.github.io/lacing/objects.inv): the Sphinx inventory: a symbol-to-URL index (`sphobjinv convert plain objects.inv -`)


# api.html.md

# API reference

| [`lacing`](_autosummary/lacing.html.md#module-lacing)   | lacing — interval annotation system.   |
|-------------------------------------------------------------------------|----------------------------------------|


