artifact-store/workplans/ARTIFACT-STORE-WP-0001-service-baseline.md

320 lines
11 KiB
Markdown
Raw Normal View History

2026-05-15 20:08:32 +02:00
---
id: ARTIFACT-STORE-WP-0001
type: workplan
title: "Foundation: Scaffold, Core Kernels, Local FS Backend"
2026-05-15 20:08:32 +02:00
repo: artifact-store
domain: infotech
status: done
2026-05-15 20:08:32 +02:00
owner: codex
topic_slug: stack
planning_priority: high
planning_order: 1
created: "2026-05-15"
updated: "2026-05-16"
2026-05-15 20:08:32 +02:00
state_hub_workstream_id: "aebf996c-8721-4e8c-9e56-61d5e4bf8dcb"
---
# ARTIFACT-STORE-WP-0001: Foundation — Scaffold, Core Kernels, Local FS Backend
2026-05-15 20:08:32 +02:00
## Purpose
Stand up the smallest credible `artifact-store` core. By the end of
this workplan, the library can ingest a directory of files into a
package, compute dual digests, write canonical-CBOR manifests, persist
state through the append-only event log, store bytes on local
filesystem, and replay materialised views from the event log. No HTTP
API yet (that lands in WP-0002); a `/health` endpoint exists so that
the dev loop has something to hit.
2026-05-15 20:08:32 +02:00
The shape is **library-first** (ffmpeg-style). HTTP server and CLI are
explicitly thin consumers of `artifactstore.registry`.
2026-05-15 20:08:32 +02:00
## Constraints (must satisfy)
2026-05-15 20:08:32 +02:00
- ADR-0001 — content-addressed storage with dual digest.
- ADR-0002 — append-only event log as source of truth.
- ADR-0003 — manifest canonicalisation = canonical CBOR.
- ADR-0004 — control plane / data plane SPI named.
- ADR-0005 — v1 technology stack pinned (Python 3.12, uv, FastAPI,
SQLAlchemy Core, asyncpg, alembic, cbor2, blake3, ruff, mypy, pytest).
- ADR-0006 — OCI compatibility kept reachable.
- `docs/ARCHITECTURE-BLUEPRINT.md` data model and module layout.
2026-05-15 20:08:32 +02:00
## Boundary
2026-05-15 20:08:32 +02:00
This workplan builds the library and a minimal `/health` endpoint. It
does NOT implement: package CRUD HTTP API (WP-0002), retention rules
beyond the seed (WP-0003), S3-compatible backend (WP-0004), guide-board
producer wiring (WP-0005), GC of unreferenced bytes (WP-0006).
2026-05-15 20:08:32 +02:00
## Target architecture (this workplan)
2026-05-15 20:08:32 +02:00
```text
artifactstore (library)
identity ──┐
manifest ──┼──> registry (orchestrator) ──> events (WAL + views)
events ───┘ │
retention (seed only) └──> dataplane.spi ──> dataplane.inproc ──> storage.spi ──> storage.backends.local
audit (view) └──> filesystem
storage.spi
dataplane.spi + inproc
api.http (just /health)
cli (just `artifactstore version`, `artifactstore migrate`, `artifactstore replay`)
2026-05-15 20:08:32 +02:00
```
## D1.1 - Service Scaffold And Repository Identity
```task
id: ARTIFACT-STORE-WP-0001-T001
status: done
2026-05-15 20:08:32 +02:00
priority: high
state_hub_task_id: "84209430-ec3b-4c5e-924e-019c25434230"
```
Acceptance:
- `pyproject.toml` with `hatchling` build backend, pinned dependencies
per ADR-0005.
- `uv.lock` committed.
- `Makefile` exposes: `make dev`, `make test`, `make lint`, `make
type`, `make migrate`. Each target is a thin shim, no logic inline.
- `src/artifactstore/` package skeleton matches ADR-0005's layout
(empty `__init__.py` and one placeholder module per top-level
concern: `identity`, `manifest`, `events`, `retention`, `audit`,
`storage`, `dataplane`, `registry`, `api/http`, `cli`, `config`).
- `tests/{unit,integration}/conftest.py` in place.
- `.env.example` documents required environment variables:
`ARTIFACTSTORE_DATABASE_URL`, `ARTIFACTSTORE_STORAGE_LOCAL_ROOT`,
`ARTIFACTSTORE_LOG_LEVEL`.
- CI-equivalent local commands: `make lint && make type && make test`
pass on a clean checkout.
- `README.md` replaces the seed README: install with `uv sync`, run
with `make dev`, test with `make test`, links to ADRs and blueprint.
## D1.2 - Digest Abstraction And Content Address
2026-05-15 20:08:32 +02:00
```task
id: ARTIFACT-STORE-WP-0001-T009
status: done
2026-05-15 20:08:32 +02:00
priority: high
state_hub_task_id: "4dc465c5-5c14-412d-b8c0-aa84076e4560"
2026-05-15 20:08:32 +02:00
```
Acceptance:
- `identity.Digest` value type with `algorithm: str` and `hex: str`,
immutable, hashable.
- `identity.ContentAddress` — string-form `<algorithm>:<hex>` with
validating parser and emitter.
- `identity.digest_stream(reader) -> {primary: Digest, sha256: Digest}`
single-pass dual-hash over an `AsyncIterator[bytes]`. Default primary
algorithm: `blake3`.
- Algorithm registry with `blake3` and `sha256` registered at import.
- Property test: digest over random byte sequences round-trips through
serialisation; `sha256` matches `hashlib.sha256(...).hexdigest()`;
`blake3` matches `blake3.blake3(...).hexdigest()`.
2026-05-15 20:08:32 +02:00
## D1.3 - Manifest Codec (Canonical CBOR + JCS Projection)
2026-05-15 20:08:32 +02:00
```task
id: ARTIFACT-STORE-WP-0001-T010
WP-0001-T010: manifest model, canonical CBOR codec, JCS projection Adds the manifest layer per ADR-0003. The canonical wire format is CBOR with deterministic encoding (cbor2 canonical=True: definite-length, shortest-form integers, sorted map keys); JCS (RFC 8785) is the JSON projection. src/artifactstore/manifest/: - model.py: frozen dataclasses for Manifest (manifest_version=1, package, files, storage_receipts, retention_summary, provenance) with restricted types (str/int/bool/None/list/dict) so CBOR and JCS round-trip losslessly. - codec.py: encode (Manifest -> canonical CBOR bytes) and decode (CBOR bytes -> Manifest) via cbor2. - projection.py: jcs_projection (Manifest -> RFC 8785 canonical JSON) plus cbor_from_jcs for cross-format round-trip verification. - digest.py: manifest_digest returns the BLAKE3 content address of the manifest's canonical CBOR bytes (ADR-0001). - __init__.py: re-exports the public surface. tests/unit/test_manifest.py: - decode(encode(m)) == m round-trip (hypothesis-parameterised). - JCS↔CBOR round-trip: encode(decode(cbor_from_jcs(jcs(m)))) == encode(m). - Byte stability of the canonical CBOR encoder across calls. - manifest_digest matches independent BLAKE3 over encode(m). - Decode rejects non-map CBOR. - JCS projection sorts keys lexicographically. Deps: jcs added to project requirements; mypy override for the jcs package (no stubs published yet). Gates: ruff clean, mypy --strict clean on 26 files, 26 tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 01:39:42 +02:00
status: done
2026-05-15 20:08:32 +02:00
priority: high
state_hub_task_id: "8b45a3d9-aa19-4ae8-afe0-687417bf12d0"
2026-05-15 20:08:32 +02:00
```
Acceptance:
- `manifest.Manifest` dataclass with the v1 fields enumerated in the
blueprint (`manifest_version=1`, package, files, storage_receipts,
retention_summary, provenance).
- `manifest.codec.encode(m) -> bytes` produces canonical CBOR
(RFC 8949 §4.2.2): definite-length, shortest-form integers,
sorted map keys.
- `manifest.codec.decode(b) -> Manifest`.
- `manifest.projection.jcs(m) -> bytes` produces RFC 8785 canonical
JSON.
- Property test: `decode(encode(m)) == m` for randomly-generated
manifests; `encode(decode(jcs_to_cbor(jcs(m)))) == encode(m)`.
- Manifest digest helper: `manifest_digest(m) -> ContentAddress` using
BLAKE3 over the canonical CBOR bytes.
## D1.4 - Registry Data Model And Migrations
2026-05-15 20:08:32 +02:00
```task
id: ARTIFACT-STORE-WP-0001-T002
WP-0001-T002: registry data model, Alembic, initial migration with retention seed Schema (src/artifactstore/db/schema.py): - events table (ADR-0002 source of truth): sequence BIGSERIAL PK, created_at, event_type, subject_kind, subject_id, actor, payload (CBOR bytes), payload_digest. Indexes on (subject_kind, subject_id) and (event_type, sequence). - artifact_packages, artifact_files, storage_locations, retention_state (materialised views over events). - retention_classes (seed table) and metadata_schemas (config table). - ADR-0001 columns present: digest_algorithm, digest_primary, digest_sha256, content_address. Blueprint tiering columns present: retrieval_tier (default 'hot'), restore_status. - Types portable: SQLAlchemy 2.0 Core with JSON().with_variant(JSONB, 'postgresql'), Uuid, LargeBinary, DateTime(timezone=True), Boolean false() default. Seed (src/artifactstore/db/seed.py): five v1 retention classes (transient, raw-evidence, summary-evidence, release-evidence, permanent-record) with default durations in seconds; permanent-record has no expiry. Alembic: - alembic.ini with sync sqlite URL default; path_separator=os to silence the 1.13 deprecation warning. - migrations/env.py: translates async URLs (+aiosqlite, +asyncpg) to sync counterparts at migrate-time so a single ARTIFACTSTORE_DATABASE_URL works for both runtime (async) and Alembic (sync). - migrations/script.py.mako template. - migrations/versions/20260516_0001_initial.py: metadata.create_all + bulk insert of retention class seeds. Make: - make migrate: alembic upgrade head (ensures var/ exists). - make migrate-fresh: drop local SQLite + re-run. Deps: psycopg[binary] added as optional `postgres` extra (PostgreSQL prod path; SQLite default for dev needs no extra). Tests: - tests/unit/test_db_schema.py: every expected table present; ADR-0001 and tiering columns present; seed has the five v1 classes; permanent-record has no default_duration; create_all + FK insert + Boolean default round-trip on in-memory SQLite. - tests/integration/test_migrations.py: alembic upgrade head against a tempfile SQLite produces all tables (+ alembic_version) and the seed rows. Gates: ruff clean, mypy --strict clean on 32 files, 38 tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 01:50:38 +02:00
status: done
2026-05-15 20:08:32 +02:00
priority: high
state_hub_task_id: "e5249a39-46a2-4b56-813e-0339c52cd14e"
2026-05-15 20:08:32 +02:00
```
Acceptance:
- Alembic configured with `migrations/` directory; `alembic upgrade
head` works against both SQLite (dev) and PostgreSQL (prod).
- `events`, `artifact_packages`, `artifact_files`, `storage_locations`,
`retention_classes`, `retention_state`, `metadata_schemas` tables
match the blueprint schema.
- Seed migration populates `retention_classes` with the five v1 entries.
- A `make migrate` and `make migrate-fresh` target work end-to-end on
a clean DB.
- All schema columns required by ADR-0001 (`digest_algorithm`,
`digest_primary`, `digest_sha256`, `content_address`), ADR-0002
(full `events` table), and the blueprint's `retrieval_tier` and
`restore_status` are present.
## D1.5 - Event Log Persistence And Replay
```task
id: ARTIFACT-STORE-WP-0001-T011
WP-0001-T011: append-only event log — write, fetch_since, tail, replay src/artifactstore/events/: - model.py: Event frozen dataclass (event_type, subject_kind, subject_id, actor, payload, payload_digest; sequence + created_at populated by the DB on write). make_event() helper computes payload_digest as raw BLAKE3 (32 bytes) of payload. ViewWriter Protocol with reset() + apply(). - log.py: * write(connection, event) — inserts one row in the caller's transaction and returns Event with sequence + created_at populated via RETURNING. * fetch_since(connection, since_sequence, limit) — read events after a cursor in order. * tail(engine, since_sequence) — async-iterator long-poll over the log; SQLite uses interval polling, PG LISTEN/NOTIFY is a future workplan. * replay(engine, view_writer, reset=True) — drains the event log through a ViewWriter inside one transaction; returns the highest sequence applied. - views.py: RegistryViewWriter — canonical event handlers shared by direct write and replay paths. Ships handlers for v1.package.created (inserts artifact_packages + retention_state) and v1.package.finalized (updates status, finalized_at, manifest_digest). Unknown event types tolerated; additional handlers register here as later tasks land. src/artifactstore/db/schema.py: events.sequence type is now BigInteger().with_variant(Integer(), 'sqlite') so SQLite's autoincrement (INTEGER PRIMARY KEY rowid alias) works while PostgreSQL keeps BIGSERIAL. tests/integration/test_event_log.py (6 cases): - write() assigns monotonic sequence numbers (1, 2, ...) and a created_at. - fetch_since(since_sequence=2) returns the ordered tail. - tail() yields events and exits cleanly on consumer break. - Direct write path (write + apply) and replay path produce byte-identical materialised state — the key ADR-0002 invariant. - Replay handles multiple event types (package.created -> finalized). - Unknown event types are tolerated (no-op apply). - payload_digest equals BLAKE3 of payload. Gates: ruff clean, mypy --strict clean on 36 files, 45 tests pass. make migrate-fresh end-to-end ok. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 01:56:04 +02:00
status: done
priority: high
state_hub_task_id: "90fce17d-cce5-4687-ae9e-02abd7d92622"
```
Acceptance:
2026-05-15 20:08:32 +02:00
- `events.write(transaction, Event)` writes one row in the given DB
transaction. Sequence numbers are assigned by the DB
(`BIGSERIAL`) and are guaranteed monotonic and gapless within a
registry instance.
- `events.tail(since_sequence) -> AsyncIterator[Event]` long-polls
the table (notify-style on PostgreSQL via `LISTEN/NOTIFY`,
poll-style on SQLite).
- `events.replay(into=ViewWriter)` rebuilds all materialised view
tables from `events` deterministically.
- Test: ingesting a fixed sequence of events, then rebuilding the
views from scratch, yields byte-identical materialised state.
- Event payloads use canonical CBOR (`manifest.codec`) so the same
bytes flow through registry → DB → tail consumer without re-encoding.
## D1.6 - Storage Adapter SPI And Local Filesystem Backend
2026-05-15 20:08:32 +02:00
```task
id: ARTIFACT-STORE-WP-0001-T003
WP-0001-T003: storage adapter SPI and local filesystem backend src/artifactstore/storage/: - spi.py: StorageBackend Protocol (backend_id, put, get, head, delete, health) and result dataclasses (StorageReceipt, StorageObjectMetadata, DeletionResult, BackendStatus). ObjectNotFoundError exception type. - registry.py: backend lookup by string ID (register/get/list_backends/ clear) per ADR-0004. - backends/local.py: LocalBackend implementation. * Object layout <root>/<algorithm>/<hex[0:2]>/<hex[2:4]>/<hex>. * Atomic writes: tmpfile + fsync + rename (idempotent re-puts drain the stream without rewriting). * Defence in depth: resolves the final path and asserts it remains under the configured root. * Range reads honour HTTP-style inclusive (start, end) tuples. * health() returns disk usage via shutil.disk_usage and surfaces an unhealthy status when the root has disappeared. * delete() cleans up emptied shard directories opportunistically. tests/unit/test_storage_local.py (14 cases): put/get round-trip; object key layout matches blueprint; head returns metadata; head/get missing raise ObjectNotFoundError; put is idempotent; delete returns True then False; range read returns subrange; range read rejects invalid range; health reports disk usage; health reports unhealthy when root vanished; ContentAddress validation blocks path-traversal-flavoured inputs; registry register/get/list/clear round-trip; idempotent re-put leaves bytes intact. Gates: ruff clean, mypy --strict clean on 41 files, 59 tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 02:01:25 +02:00
status: done
2026-05-15 20:08:32 +02:00
priority: high
state_hub_task_id: "68f9a752-0012-4cc1-8768-ec3f75295e7a"
2026-05-15 20:08:32 +02:00
```
Acceptance:
- `storage.spi.StorageBackend` Protocol matches the blueprint.
- `storage.backends.local.LocalBackend` implements the SPI:
- Object key layout `<root>/<algo>/<hex[0:2]>/<hex[2:4]>/<hex>`.
- Atomic write via `fsync(tmpfile) + rename`.
- Path traversal rejected at the SPI boundary.
- `health()` returns disk usage and root accessibility.
- Backend registry resolves by `backend_id` string (per ADR-0004).
- Unit tests cover: put, get, head, delete, double-put idempotency,
delete-of-missing, range read.
2026-05-15 20:08:32 +02:00
## D1.7 - Data Plane SPI And In-Process Implementation
2026-05-15 20:08:32 +02:00
```task
id: ARTIFACT-STORE-WP-0001-T012
WP-0001-T012: data plane SPI and in-process implementation src/artifactstore/dataplane/: - spi.py: DataPlane Protocol with the five operations ingest_stream, serve_object, verify_object, delete_object, backend_health (ADR-0004). Dataclasses: IngestHints (size_hint, primary_algorithm, backend_id overrides), IngestResult (primary_digest + sha256_digest + size_bytes + StorageReceipt), VerifyResult (verified bool, mismatch reason, actual digests + size). - inproc.py: InProcessDataPlane wraps one StorageBackend. ingest_stream is two-pass against a tempfile (drain stream while dual-hashing into BLAKE3+SHA-256, then forward the tempfile to backend.put under the primary content address); fsync+cleanup on exception. serve_object passes byte ranges through; verify_object re-reads bytes via backend.get, re-digests with the stored algorithm, and reports mismatches. delete and health are thin pass-throughs. tests/unit/test_dataplane_inproc.py (11 cases): - ingest_stream computes correct dual digests, returns receipt, stores bytes at the content-addressed path. - empty-input ingest returns the BLAKE3/SHA-256 of empty. - serve_object round-trips ingested bytes; supports byte_range. - verify_object verifies intact bytes; detects on-disk corruption. - delete_object passes through (True then False). - backend_health passes through. - IngestHints override of primary_algorithm (sha256-as-primary path). - Missing-object serve raises ObjectNotFoundError. - Architectural test (ADR-0004 invariant): no control-plane module (api / registry / retention / audit) imports artifactstore.storage.backends.* or artifactstore.dataplane.inproc directly. Enforced via AST scan of every .py file in those packages. Gates: ruff clean, mypy --strict clean on 44 files, 70 tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 02:07:05 +02:00
status: done
priority: high
state_hub_task_id: "8cb8a245-beb5-4713-8d1d-8a623431ad81"
2026-05-15 20:08:32 +02:00
```
Acceptance:
- `dataplane.spi.DataPlane` Protocol matches ADR-0004.
- `dataplane.inproc.InProcessDataPlane` implements all five operations
on top of a configured `StorageBackend`.
- `ingest_stream` computes both digests in a single pass, writes to
the backend keyed by the primary content address, and returns an
`IngestResult` containing both digests, size, and the
`StorageReceipt`.
- `serve_object` and `verify_object` re-read bytes through the
backend; `verify_object` re-digests and returns mismatches if any.
- Lint rule (or test): no code outside `dataplane.*` imports
`storage.backends.*` directly.
2026-05-15 20:08:32 +02:00
## D1.8 - Registry Orchestrator (Library Surface)
2026-05-15 20:08:32 +02:00
```task
id: ARTIFACT-STORE-WP-0001-T013
WP-0001-T013: registry orchestrator (library surface) src/artifactstore/registry/__init__.py implements the Registry class with six operations the HTTP API and CLI both consume: * create_package(name, producer, subject, retention_class, actor, metadata?) -> UUID. Validates retention_class against the seed table; emits v1.package.created with CBOR payload; applies view in same transaction. * ingest_file(package_id, relative_path, media_type, stream, actor) -> UUID. Validates the package is in 'created' status and rejects duplicate relative_path. Calls dataplane.ingest_stream (which dual-hashes and writes to the backend). Emits v1.file.ingested whose payload carries the file metadata + storage receipt + deterministic storage_location_id so replay reproduces UUIDs. View handler in events/views.py inserts artifact_files + storage_locations and bumps last_event_sequence on the package. * finalize_package(package_id, actor) -> ContentAddress. Queries the views to build a Manifest dataclass, encodes it as canonical CBOR, computes the BLAKE3 content address, and writes v1.package.finalized whose payload IS the canonical CBOR manifest. The view handler now records manifest_digest = event.payload_digest (BLAKE3 of the manifest), not a separate field parsed from the payload. * get_manifest_bytes(package_id, format='cbor'|'json') -> bytes. Reads the finalize event payload (CBOR) and optionally projects to JCS. * get_file(file_id) -> AsyncIterator[bytes]. Looks up the storage location and serves bytes via the data plane. * tail_events(since_sequence, poll_interval_seconds) -> AsyncIterator[Event]. Pass-through to events.tail. src/artifactstore/events/views.py: - New v1.file.ingested handler. - v1.package.finalized handler updated: manifest_digest now derived from event.payload_digest (= BLAKE3 of the canonical CBOR manifest payload). - All inserts now pass created_at=event.created_at explicitly so replay produces byte-identical materialised state (server_default=now() was firing fresh on each replay insert). tests/integration/test_registry.py (7 cases): - Rejects unknown retention class. - create_package writes the event and the package row. - ingest_file writes file + storage_location, populates content_address with blake3 prefix. - Duplicate relative_path raises DuplicateRelativePathError. - ingest into unknown package raises PackageNotFoundError. - Finalising twice raises IllegalPackageStateError. - End-to-end: create + ingest 3 files + finalize + read manifest in CBOR and JSON + download each file with byte equality + tail 5 events + replay + assert byte-identical materialised state across pre and post snapshots. tests/integration/test_event_log.py updated: the v1.package.finalized replay test now uses the new payload semantics (payload is the canonical CBOR manifest; manifest_digest = BLAKE3 of payload). Gates: ruff clean, mypy --strict clean on 45 files, 77 tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 08:17:40 +02:00
status: done
2026-05-15 20:08:32 +02:00
priority: high
state_hub_task_id: "f4967308-4613-4def-8c09-41caaeb631f7"
2026-05-15 20:08:32 +02:00
```
Acceptance:
- `registry.Registry` exposes: `create_package`, `ingest_file`,
`finalize_package`, `get_manifest_bytes` (CBOR + JCS), `get_file`,
`tail_events`. Plus stubs for the retention operations that lighten
WP-0003.
- Each mutating operation is one DB transaction that writes events
AND updates materialised views.
- Finalisation writes one `v1.package.finalized` event whose payload
*is* the canonical CBOR manifest, and stamps `manifest_digest` on
`artifact_packages`.
- Duplicate `relative_path` within one not-yet-finalised package is
rejected unless an explicit replace is requested.
- Integration test: end-to-end ingest of a 3-file package against
local backend → finalize → read manifest → verify digests
→ tail events → replay rebuilds identical state.
## D1.9 - Minimal HTTP App And CLI
```task
id: ARTIFACT-STORE-WP-0001-T014
WP-0001-T014: minimal HTTP app and CLI src/artifactstore/app.py (new): composition root. build_registry(settings) wires AsyncEngine + LocalBackend + InProcessDataPlane + RegistryViewWriter into a Registry. Used by both the HTTP app and the CLI. src/artifactstore/registry/__init__.py: adds db_health() (SELECT 1 probe), backend_health() (pass-through to dataplane), and dispose() (engine shutdown) helpers so the HTTP /health endpoint and CLI commands can talk to the registry without reaching for private state. src/artifactstore/api/http/__init__.py: - create_app(settings=None) factory; lifespan owns the registry instance and disposes it on shutdown. - GET / returns the scaffold banner. - GET /health reports overall status + db {healthy, detail} + backend {backend_id, healthy, detail, free_bytes, total_bytes}. Uses FastAPI Depends() with a request->state.registry helper rather than reaching app.state directly. - Module-level `app = create_app()` so `uvicorn artifactstore.api.http:app` keeps working. src/artifactstore/cli/__init__.py: - migrate: `alembic upgrade head` via the alembic command API. - replay: drops + rebuilds materialised views from the event log; prints the highest applied sequence. - health: prints the same payload as the HTTP /health endpoint, as JSON. - version unchanged. Tests: - tests/integration/test_http_health.py (TestClient-based): / scaffold banner; /health reports ok with db.healthy + backend.healthy + free_bytes populated. - tests/integration/test_cli_commands.py (typer CliRunner): version prints; migrate creates the schema (events + retention_classes + alembic_version); replay against an empty log exits ok with "replayed up to sequence 0"; health prints a status=ok JSON payload. Gates: ruff clean, mypy --strict clean on 48 files, 83 tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 08:56:13 +02:00
status: done
priority: medium
state_hub_task_id: "a43628ab-8b53-45fa-852a-ff0118dd12e7"
```
Acceptance:
- `api.http.app` is a FastAPI app with one route: `GET /health`
reporting registry liveness, DB connectivity, and backend health.
- `cli` exposes `artifactstore version`, `artifactstore migrate`,
`artifactstore replay`, `artifactstore health`.
- `make dev` starts the API on `127.0.0.1:8000` with SQLite +
local FS backend by default.
2026-05-15 20:08:32 +02:00
## D1.10 - Operator Documentation And ADR Cross-Linking
2026-05-15 20:08:32 +02:00
```task
id: ARTIFACT-STORE-WP-0001-T008
status: done
2026-05-15 20:08:32 +02:00
priority: medium
state_hub_task_id: "9b60036c-61f2-4c22-ad31-7213473d42d0"
```
Acceptance:
- `README.md` updated with current run / test / migrate commands.
- `AGENTS.md` "Current Repo Shape" section reflects the scaffold.
- An `docs/OPERATOR.md` page documents environment variables, local
vs PostgreSQL setup, replay command, and a smoke-test recipe.
- Every ADR is cross-linked from at least one of: blueprint, this
workplan, or `OPERATOR.md`.
## Suggested implementation order
1. T001 — scaffold and tooling (no other task can start without this).
2. T009 — digest abstraction (unblocks T010, T012).
3. T010 — manifest codec (unblocks T013).
4. T002 — schema and migrations (unblocks T011, T013).
5. T011 — event log + replay.
6. T003 — storage SPI + local backend.
7. T012 — data plane SPI + in-process impl.
8. T013 — registry orchestrator.
9. T014 — minimal HTTP app and CLI.
10. T008 — docs.
## Success criteria
- `make dev && make test` round-trips on a clean checkout.
- A scripted integration test ingests a directory of fixture files,
finalises the package, reads the manifest, downloads each file, and
verifies digests end-to-end against the local backend.
- Replaying events from sequence 1 reproduces the materialised view
state byte-for-byte.
- The library can be imported and exercised without an HTTP server
running (embedding test).