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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,6 @@ jobs:
needs: [ts-build] # Python tests need TypeScript compiled
steps:
- uses: actions/checkout@v4
with:
submodules: true # vendor/layers lexicons for interop validation

- name: Install pnpm
uses: pnpm/action-setup@v4
Expand Down
5 changes: 0 additions & 5 deletions .gitmodules

This file was deleted.

36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.7.0] - 2026-06-29

### Added

#### Deep `layers` integration through `lairs`

- `bead.interop.layers` builds its layers projections from the canonical
`lairs.records` models that `lairs` generates from the layers lexicons, so the
schema has one source of truth. The bridge, parse, graph, and resource lenses
return typed `lairs` models (or small bead-side aggregates of them) as their
view; the lens complement carries the bead-only remainder, and the round-trip
is guaranteed by the didactic GetPut/PutGet laws.
- `ItemLayersLens` (`ITEM_LAYERS`, `item_to_layers`) maps an `Item`'s standoff
spans and relations to span and relation `AnnotationLayer` records over
per-element expressions and tokenizations: a span anchors by
`tokenRefSequence` (with `head_index` as `anchorTokenIndex` and a Wikidata
`label_id` as a `knowledgeRef`); a relation carries `ArgumentRef` source and
target arguments.
- `BeadCodec`, registered on the `lairs.codecs` entry point, round-trips a bead
`ItemCollection` to and from the layers schema. `lairs.codec("bead")` resolves
it, and `encode(decode(x)) == x` holds losslessly.
- `bead.interop.layers.corpus_io` ingests a `lairs.data.Corpus` into bead corpus
models (`corpus_to_records`, `corpus_to_graph`, `corpus_to_items`) and emits
bead data as a corpus (`items_to_corpus`, `graph_to_corpus`,
`materialize_corpus`, `save_corpus_repo`, `publish_corpus`).
- `bead layers` CLI commands: `encode`, `decode`, `materialize`, and an opt-in
`publish` that defaults to a dry run.

### Changed

- `lairs>=0.5.0` is a dependency; the minimum `didactic` is raised to `>=0.9.0`
and `panproto` to `>=0.56.0`. Importing `bead` does not import `lairs`; the
dependency loads only when `bead.interop.layers` is used.
- Layers record validation is owned by `lairs`, whose generated models validate
on construction.

## [0.6.0] - 2026-05-29

### Added
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,20 @@ uv pip install bead[training] # PyTorch Lightning, TensorBoard
### Development

```bash
git clone --recurse-submodules https://github.com/FACTSlab/bead.git
git clone https://github.com/FACTSlab/bead.git
cd bead
uv sync --all-extras
uv run pytest tests/
```

The `vendor/layers` submodule holds the layers lexicons that the interop tests
validate against. If you cloned without `--recurse-submodules`, fetch them with
`git submodule update --init vendor/layers`, and refresh to the latest published
lexicons with `git submodule update --remote vendor/layers`. The lexicon
validation tests skip automatically when the submodule is absent.

Always use `uv run` to execute commands.

Integration tests that exercise the layers publish path against a real ATProto
PDS are deselected by default. With docker running, opt in with
`uv run pytest --run-integration`; the test stands up a throwaway
[bluesky PDS](https://github.com/bluesky-social/pds) container and skips cleanly
when docker is unavailable.

## Quick Start

```python
Expand Down
2 changes: 1 addition & 1 deletion bead/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@

from __future__ import annotations

__version__ = "0.6.0"
__version__ = "0.7.0"
__author__ = "Aaron Steven White"
__email__ = "aaron.white@rochester.edu"
100 changes: 100 additions & 0 deletions bead/cli/layers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Layers interop commands for the bead CLI.

Convert a bead ``ItemCollection`` to and from the ``layers`` schema through the
``bead`` lairs codec, materialize a collection as an Arrow/Parquet corpus, and
publish a committed corpus revision to an ATProto PDS.
"""

from __future__ import annotations

from pathlib import Path

import click
from lairs.integrations.codecs import CorpusFragment
from lairs.store import Repository
from rich.console import Console

from bead.cli.utils import print_info, print_success
from bead.interop.layers.codec import BeadCodec
from bead.interop.layers.corpus_io import (
items_to_corpus,
materialize_corpus,
publish_corpus,
)
from bead.items.item import ItemCollection

console = Console()


@click.group()
def layers() -> None:
r"""Layers interop commands.

Convert a bead item collection to and from the ``layers`` schema and
materialize it as an Arrow/Parquet corpus.

\b
Examples:
$ bead layers encode items.json --out fragment.json
$ bead layers decode fragment.json --out items.json
$ bead layers materialize items.json --out corpus/
"""


@layers.command()
@click.argument("items_file", type=click.Path(exists=True, path_type=Path))
@click.option("--out", "out_file", type=click.Path(path_type=Path), default=None)
def encode(items_file: Path, out_file: Path | None) -> None:
"""Encode an ItemCollection JSON document into a layers fragment."""
fragment = BeadCodec().decode(items_file.read_text())
payload = fragment.model_dump_json()
if out_file is None:
console.print_json(payload)
else:
out_file.write_text(payload)
print_success(f"Wrote layers fragment to {out_file}")


@layers.command()
@click.argument("fragment_file", type=click.Path(exists=True, path_type=Path))
@click.option("--out", "out_file", type=click.Path(path_type=Path), default=None)
def decode(fragment_file: Path, out_file: Path | None) -> None:
"""Decode a layers fragment back into an ItemCollection JSON document."""
fragment = CorpusFragment.model_validate_json(fragment_file.read_text())
payload = BeadCodec().encode(fragment.records)
if out_file is None:
console.print_json(payload)
else:
out_file.write_text(payload)
print_success(f"Wrote item collection to {out_file}")


@layers.command()
@click.argument("items_file", type=click.Path(exists=True, path_type=Path))
@click.option("--out", "out_dir", type=click.Path(path_type=Path), required=True)
@click.option("--name", default="corpus", help="Corpus name")
def materialize(items_file: Path, out_dir: Path, name: str) -> None:
"""Materialize an ItemCollection as an Arrow/Parquet layers corpus."""
collection = ItemCollection.model_validate_json(items_file.read_text())
corpus = items_to_corpus(collection, corpus_name=name)
out_dir.mkdir(parents=True, exist_ok=True)
paths = materialize_corpus(corpus, out_dir)
for path in paths:
print_info(f" {path}")
print_success(f"Materialized {len(paths)} view(s) to {out_dir}")


@layers.command()
@click.argument("repo_path", type=click.Path(exists=True, path_type=Path))
@click.argument("revision")
@click.option("--to", "to_did", required=True, help="Target PDS DID")
@click.option("--dry-run/--no-dry-run", default=True, help="Plan only (default)")
def publish(repo_path: Path, revision: str, to_did: str, dry_run: bool) -> None:
"""Publish a committed corpus revision to a PDS (dry run by default)."""
repo = Repository.open(repo_path)
plan = publish_corpus(repo, revision, to=to_did, dry_run=dry_run)
if dry_run:
print_info(str(plan))
print_success("Computed publish plan (dry run; nothing written)")
else:
print_success(f"Published revision {revision} to {to_did}")
1 change: 1 addition & 0 deletions bead/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ def _lazy_load(self, cmd_name: str) -> click.Command:
"config": ("bead.cli.config", "config"),
"deployment": ("bead.cli.deployment", "deployment"),
"items": ("bead.cli.items", "items"),
"layers": ("bead.cli.layers", "layers"),
"lists": ("bead.cli.lists", "lists"),
"models": ("bead.cli.models", "models"),
"protocol": ("bead.cli.protocol", "protocol"),
Expand Down
85 changes: 56 additions & 29 deletions bead/interop/layers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
"""Lossless, law-verified lenses between bead models and the ``layers`` schema.

Maps bead's corpus and annotation models to ``layers``-shaped JSON and back via
didactic lenses (``dx.Lens``/``dx.Iso``): the layers view is a faithful,
standalone projection; the lens complement holds the bead-only round-trip
remainder. Round-trip fidelity is guaranteed by the didactic GetPut/PutGet laws.
Maps bead's corpus, annotation, and resource models to and from the canonical
``layers`` record models generated by ``lairs`` (``lairs.records``) via didactic
lenses (``dx.Lens``/``dx.Iso``): the layers view is a faithful, standalone
projection built from the generated models; the lens complement holds the
bead-only round-trip remainder. Round-trip fidelity is guaranteed by the
didactic GetPut/PutGet laws.

What is mapped:

- The linguistic ``layers`` constructs (the shared object definitions and the
expression, segmentation, annotation, graph, media, and ontology records) are
mirrored as didactic models with a generic ``MirrorIso`` (see ``models``,
``models_records``, and ``model_lenses``).
- bead's pipeline outputs bridge directly to layers: ``CorpusGraph`` to the
property graph, ``CorpusRecord`` to an ``expression``, and a dependency parse
to a ``tokenization`` with part-of-speech and dependency annotation layers.
- bead's resources map to their layers counterparts: ``LexicalItem`` to an
- ``CorpusRecord`` to a layers ``expression`` (see ``bridges``).
- ``CorpusGraph`` to the layers property graph (expressions, graph nodes, and a
``graphEdgeSet``) bundled as a :class:`CorpusGraphLayers` view (see
``graph_lens``).
- a dependency parse to a layers ``tokenization`` with part-of-speech and
dependency annotation layers bundled as a :class:`ParsedSentenceLayers` view
(see ``parse_lens``).
- an ``Item``'s spans and relations to span and relation annotation layers (see
``item_bridge``), with a ``bead`` lairs codec over an ``ItemCollection`` (see
``codec``) and corpus ingest/egress helpers (see ``corpus_io``).
- bead's resources to their layers counterparts: ``LexicalItem`` to an
``entry``, ``Lexicon`` to a ``collection``, and ``Template`` to a ``template``
(see ``resource_lens``).
"""
Expand All @@ -26,22 +31,33 @@
RecordExpressionLens,
record_to_expression,
)
from bead.interop.layers.codec import BeadCodec
from bead.interop.layers.corpus_io import (
corpus_to_graph,
corpus_to_items,
corpus_to_records,
expression_to_record,
graph_to_corpus,
items_to_corpus,
load_layers_corpus,
materialize_corpus,
publish_corpus,
save_corpus_repo,
)
from bead.interop.layers.graph_lens import (
CORPUS_GRAPH_LAYERS,
CorpusGraphLayers,
CorpusGraphLayersLens,
graph_to_layers,
)
from bead.interop.layers.model_lenses import (
ALL_MIRROR_ISOS,
RECORD_ISOS,
RECORD_MODELS,
SHARED_DEF_ISOS,
SHARED_DEF_MODELS,
MirrorIso,
mirror_iso,
from bead.interop.layers.item_bridge import (
ITEM_LAYERS,
ItemLayersLens,
item_to_layers,
)
from bead.interop.layers.parse_lens import (
PARSED_SENTENCE_LAYERS,
ParsedSentenceLayers,
ParsedSentenceLayersIso,
parse_to_layers,
)
Expand All @@ -51,30 +67,41 @@
TEMPLATE_LAYERS,
LexicalItemEntryLens,
LexiconCollectionLens,
LexiconLayers,
TemplateLayersLens,
)

__all__ = [
"ALL_MIRROR_ISOS",
"CORPUS_GRAPH_LAYERS",
"ITEM_LAYERS",
"LEXICAL_ITEM_ENTRY",
"LEXICON_COLLECTION",
"PARSED_SENTENCE_LAYERS",
"RECORD_EXPRESSION",
"TEMPLATE_LAYERS",
"BeadCodec",
"CorpusGraphLayers",
"CorpusGraphLayersLens",
"ItemLayersLens",
"LexicalItemEntryLens",
"LexiconCollectionLens",
"TemplateLayersLens",
"RECORD_ISOS",
"RECORD_MODELS",
"SHARED_DEF_ISOS",
"SHARED_DEF_MODELS",
"CorpusGraphLayersLens",
"MirrorIso",
"LexiconLayers",
"ParsedSentenceLayers",
"ParsedSentenceLayersIso",
"RecordExpressionLens",
"TemplateLayersLens",
"corpus_to_graph",
"corpus_to_items",
"corpus_to_records",
"expression_to_record",
"graph_to_corpus",
"graph_to_layers",
"mirror_iso",
"item_to_layers",
"items_to_corpus",
"load_layers_corpus",
"materialize_corpus",
"parse_to_layers",
"publish_corpus",
"record_to_expression",
"save_corpus_repo",
]
Loading
Loading