Add artifact wire format and docbank-backed local store - #1242
Conversation
Co-authored-by: maphew <maphew@gmail.com>
Extracted from the artifact-sync reference branch as PR1 of the split: frozen wire format types and golden hashes, canonical JSON encoder, zstd wire codec, logical store interface, docbank-backed store, and open-on-demand vault repository. Export/import pipelines, transports, GC/maintenance, and metadata machinery land in later PRs. Co-authored-by: maphew <maphew@gmail.com>
Co-authored-by: maphew <maphew@gmail.com>
roborev: Combined Review (
|
TestOpenRepositoryRetainsAbsoluteCanonicalRoot built its relative data dir with filepath.Rel against the working directory, which fails on Windows CI where the checkout (D:) and temp dir (C:) live on different drives. Chdir to the temp dir's parent and open by base name so the relative path never crosses volumes. Co-authored-by: maphew <maphew@gmail.com>
roborev: Combined Review (
|
Restore the reference branch's media-type validation in Create: docbank's own authority conflict only covers mismatches against existing content, so a new artifact created with a noncanonical media type was silently persisted forever under an immutable ref. Noncanonical types on new artifacts now fail with ErrArtifactInvalid; mismatches against existing artifacts keep returning ErrArtifactConflict. Close no longer nil-writes the vault pointer, which raced concurrent store operations during shutdown. Idempotency and post-close rejection both come from docbank's lifecycle lock; mapDocbankError now surfaces those as fs.ErrClosed. Co-authored-by: maphew <maphew@gmail.com>
roborev: Combined Review (
|
DecodeWire wrapped every non-context failure as ErrArtifactCorrupt, including write errors from the caller's destination such as disk-full or permission failures, falsely branding valid wire input as corrupt. Tag destination writes with an internal sentinel and return those errors without the corruption wrap; oversize output and malformed encoding still classify as corrupt. Co-authored-by: maphew <maphew@gmail.com>
roborev: Combined Review (
|
The RawMessage canonicalization path decoded only the first JSON value,
so raw content like {"a":1}{"b":2} silently canonicalized and hashed
identically to {"a":1}, losing data behind a content-hash collision.
Require io.EOF after the first value; trailing whitespace stays accepted
and canonical bytes for valid input are unchanged, as the format goldens
confirm.
Co-authored-by: maphew <maphew@gmail.com>
roborev: Combined Review (
|
Replace the pseudo-version pin with the released tag. The delta on the consumed API is additive (optional create provenance, a Provenance read method, and an internal open-locking simplification); no signatures used by internal/artifact changed. Co-authored-by: maphew <maphew@gmail.com>
roborev: Combined Review (
|
…rors Drop quality_signals from the manifest session DTO. db.Session's pointer is load-path-transient — JSON decodes populate it while database scans fill only hidden scalar columns — so serializing it hashed the same logical session differently depending on how it was loaded. The manifest-level session_quality_signals field is the single canonical carrier (built from StoredQualitySignals, restored with ApplyQualitySignals), and reference-produced manifests never populated the inner field, so canonical bytes are unchanged and the goldens hold. The db.Session parity test documents the exemption and asserts manifest bytes are pointer-independent. Tag wire source read failures with a sentinel, mirroring the destination tagging: network resets and disk errors during decode now surface with the underlying error preserved instead of ErrArtifactCorrupt. io.EOF passes through untagged, and a source that ends cleanly but early still classifies as corrupt. Co-authored-by: maphew <maphew@gmail.com>
roborev: Combined Review (
|
Raw-source path validation accepted Windows drive-qualified and volume-relative paths written with forward slashes (C:/Users/..., C:...), which satisfy the relative-path checks on non-Windows parsers. Reject any colon in the path: one platform-neutral rule that also covers NTFS alternate data streams and subsumes the URI-scheme check. Co-authored-by: maphew <maphew@gmail.com>
roborev: Combined Review (
|
Second PR in the artifact-sync split (follows #1242, which froze the wire format and added the docbank-backed store). Extracted from the closed #1239 branch with review fixes applied during extraction. ## What this adds - **SQLite publication ledger** (`internal/db`): `artifact_export_queue`, `artifact_publications`, `artifact_publication_revisions`, `artifact_checkpoint_heads`/`_floors`, plus session triggers that enqueue owned-session changes. Triggers are origin-gated — they fire only once an artifact origin exists in `pg_sync_state` — so archives that never opt in carry no queue writes. Trigger DDL lives in Go and is installed after column migrations (drops run before), so no trigger references session columns while migrations run. - **Go enqueue hooks** for child-only mutations that don't touch trigger-covered session columns: batch message writes (queue generation sampled around the batch, enqueues exactly once when the triggers didn't fire), standalone usage-event replacement, and token-coverage backfill. Queue bootstrap is an explicit call at origin creation, not a migrate-time backfill. - **Origin lifecycle** (`internal/artifact`): `EnsureOrigin`/`AdoptOrigin`/`StoredOrigin`. Creating or adopting an origin bootstraps the export queue; a failed bootstrap rolls the origin back (deleting the key when there was none before, since the gates test key existence) so a retry re-runs population. Adopting over a different established origin force-requeues every owned session with a generation bump, because prior acknowledgements belong to the old origin. - **Checkpointed export pipeline** (`internal/artifact`): claims pending queue rows, publishes content-addressed session manifests and segments into the store, records publication revisions, and advances per-origin checkpoint heads with monotone sequence reservation. Stale claims (a writer advanced the generation mid-export) roll back atomically. Incremental export is bounded by the dirty batch; an unchanged archive costs a catalog identity check only. Full export streams all bodies, then re-checks the queue up to 32 settle rounds — hitting the bound returns the accumulated result with an error meaning "made progress, run again." - **Resync carriage**: `CopySyncStateFrom` carries queue, publication, revision, and checkpoint state across a full resync and re-dirties every copied queue row — the origin gate keeps triggers silent in the rebuild's temp DB, and a parser bump changes manifest hashes anyway, so the exporter must re-verify each session (unchanged content is cheap to skip, being content-addressed). ## Scope and limitations - Export-side only. Nothing wires the pipeline to the daemon or CLI yet; that lands with folder transport and import in a later PR. There is no import/read path here. - The ledger is local SQLite state. PostgreSQL push and shared query shapes are untouched. - The wire format is unchanged: no golden churn, format stays frozen at v1. ## Where to look - `internal/db/artifact_publication.go` — queue and ledger SQL, origin-gated enqueue - `internal/db/db.go` — trigger DDL split around column migrations, bootstrap/requeue - `internal/artifact/export.go` — publish → manifest → checkpoint ordering and claim lifecycle - `internal/artifact/origin.go` — origin lifecycle and rollback semantics - `internal/db/orphaned.go` — resync carriage and the pending-flag decision Co-authored-by: Wes McKinney <wesm@users.noreply.github.com>
Second PR in the artifact-sync split (follows kenn-io#1242, which froze the wire format and added the docbank-backed store). Extracted from the closed kenn-io#1239 branch with review fixes applied during extraction. - **SQLite publication ledger** (`internal/db`): `artifact_export_queue`, `artifact_publications`, `artifact_publication_revisions`, `artifact_checkpoint_heads`/`_floors`, plus session triggers that enqueue owned-session changes. Triggers are origin-gated — they fire only once an artifact origin exists in `pg_sync_state` — so archives that never opt in carry no queue writes. Trigger DDL lives in Go and is installed after column migrations (drops run before), so no trigger references session columns while migrations run. - **Go enqueue hooks** for child-only mutations that don't touch trigger-covered session columns: batch message writes (queue generation sampled around the batch, enqueues exactly once when the triggers didn't fire), standalone usage-event replacement, and token-coverage backfill. Queue bootstrap is an explicit call at origin creation, not a migrate-time backfill. - **Origin lifecycle** (`internal/artifact`): `EnsureOrigin`/`AdoptOrigin`/`StoredOrigin`. Creating or adopting an origin bootstraps the export queue; a failed bootstrap rolls the origin back (deleting the key when there was none before, since the gates test key existence) so a retry re-runs population. Adopting over a different established origin force-requeues every owned session with a generation bump, because prior acknowledgements belong to the old origin. - **Checkpointed export pipeline** (`internal/artifact`): claims pending queue rows, publishes content-addressed session manifests and segments into the store, records publication revisions, and advances per-origin checkpoint heads with monotone sequence reservation. Stale claims (a writer advanced the generation mid-export) roll back atomically. Incremental export is bounded by the dirty batch; an unchanged archive costs a catalog identity check only. Full export streams all bodies, then re-checks the queue up to 32 settle rounds — hitting the bound returns the accumulated result with an error meaning "made progress, run again." - **Resync carriage**: `CopySyncStateFrom` carries queue, publication, revision, and checkpoint state across a full resync and re-dirties every copied queue row — the origin gate keeps triggers silent in the rebuild's temp DB, and a parser bump changes manifest hashes anyway, so the exporter must re-verify each session (unchanged content is cheap to skip, being content-addressed). - Export-side only. Nothing wires the pipeline to the daemon or CLI yet; that lands with folder transport and import in a later PR. There is no import/read path here. - The ledger is local SQLite state. PostgreSQL push and shared query shapes are untouched. - The wire format is unchanged: no golden churn, format stays frozen at v1. - `internal/db/artifact_publication.go` — queue and ledger SQL, origin-gated enqueue - `internal/db/db.go` — trigger DDL split around column migrations, bootstrap/requeue - `internal/artifact/export.go` — publish → manifest → checkpoint ordering and claim lifecycle - `internal/artifact/origin.go` — origin lifecycle and rollback semantics - `internal/db/orphaned.go` — resync carriage and the pending-flag decision Co-authored-by: Wes McKinney <wesm@users.noreply.github.com>
PR1 of the artifact-sync split (supersedes the closed #1239, extracted and pared from its reference branch with @maphew as co-author).
This lands the foundation of
internal/artifact: the frozen v1 wire format and the local content-addressed store, with no export/import pipeline, transports, server, db, or CLI integration yet.What's here
raw_sourcecontract (SHA-256 identity, size cap,application/jsonlmedia-type allowlist, sanitized relative paths) is frozen and validated here even though capture lands in PR3.manifestSessiontodb.Session's JSON-visible fields, so incidental session-struct growth cannot silently re-hash every manifest.ArtifactStoreinterface and the Docbank-backed implementation (go.kenn.io/docbank, new dependency, plus a kit upgrade), with a contract suite that runs against both SQLite drivers (mattn and modernc).Repository: open-on-demand vault ownership under$AGENTSVIEW_DATA_DIR/artifacts. Nothing creates the vault for users who don't opt in;serveintegration comes later.config.ValidateArtifactOriginIDwith table tests.Deliberately absent, arriving in later PRs
contextArtifactReader, decoded-limit consts, quarantine listing surface, compression tests); each returns verbatim with its consumer.ErrArtifactUnsupportedis frozen store vocabulary whose returning consumer is PR6 maintenance.canonicalArtifactPathis temporarily duplicated intorepository.go; PR2's sync.go extraction must reconcile to a single copy.Quarantinekeeps itsreasonparameter (currently discarded): PR6's quarantine listing may persist it once docbank can store a reason.Where to look
internal/artifact/wire.go+format_test.go— the format freeze and golden hashes.internal/artifact/canonical_json.go— deterministic encoder (byte-identical to the reference; it determines every content hash).internal/artifact/store_docbank.go+store_contract_test.go— store semantics: immutable create, conflict detection, verified reads, quarantine/trash, two-driver matrix.🤖 Generated with Claude Code