# Completion catalog and read model
This library owns the append-only completion catalog and the projections used to
present active and completed session records. It performs local filesystem work
only. It does not delete live session files, order caller locks, commit remote
objects, execute providers, or own session lifecycle transitions.
`SessionRecord` and `SessionLog` are re-exported leaf values accepted by the
read model.
## Completion data
```rust
pub struct RecordCompletion {
pub session_object_id: String,
pub commit_receipt: Option<CompletionReceipt>,
pub session_id: Option<String>,
pub session_type: Option<String>,
pub created_at: Option<String>,
}
pub struct CompletionReceipt {
pub transaction_id: Option<String>,
pub session_object_id: String,
pub session_id: Option<String>,
pub session_type: Option<String>,
pub created_at: Option<String>,
pub committed_at: Option<String>,
pub ingress_source: Option<serde_json::Value>,
pub node_ids: std::collections::BTreeMap<String, String>,
pub object_ids: std::collections::BTreeMap<String, String>,
}
pub struct RecordedCompletion {
pub receipt: CompletionReceipt,
pub appended: bool,
}
pub enum Error {
Conflict(String),
Storage(anyhow::Error),
}
```
Input and receipt values are cloneable and debuggable; the input and receipt
values retain their existing Serde representations. Receipt JSON uses the
existing camel-case field names. A nonblank catalog line that does not begin
with `{` is a legacy session-object ID and is projected as a receipt whose other
fields are empty. Blank lines are ignored. The first receipt for an object ID
wins. A duplicate returns that first receipt with `appended: false` and does not
append or replace it. A supplied structured receipt naming an object other than
`session_object_id` is a conflict.
## Catalog
`Catalog` is cloneable.
```rust
impl Catalog {
pub fn open(path: std::path::PathBuf) -> Result<Catalog, Error>;
pub fn receipts(&self) -> Result<Vec<CompletionReceipt>, Error>;
pub fn record(
&self,
input: RecordCompletion,
committed_at: String,
) -> Result<RecordedCompletion, Error>;
}
```
`open` creates a missing parent directory with private Unix permissions, creates
and synchronizes a missing catalog file and its directory entry, and otherwise
leaves existing bytes unchanged. `receipts` parses the complete file in order
and deduplicates by session-object ID. Malformed structured lines fail the
whole read with their line context.
`record` fills absent receipt metadata from the input, fills an absent
`committed_at` from its argument, checks identity, then appends one camel-case
JSON line, flushes it, and calls `sync_data` before reporting `appended: true`.
Clones of one `Catalog` serialize reads and writes with an internal same-process
mutex; independent processes are not coordinated. No caller lock protocol is
exposed.
Catalog work is O(file bytes plus decoded receipts) per read or record, with
memory proportional to decoded receipts. A successful new record performs one
append, one flush, and one file-data synchronization. There are no retries,
timeouts, network operations, arbitrary receipt-count caps, or truncation.
Valid inputs are filesystem paths and receipts representable by the declared
UTF-8 and JSON fields. Identity mismatches return `Error::Conflict`; local
filesystem, synchronization, lock, encoding, and decoding failures return
`Error::Storage`.
## Read model
```rust
pub struct ProviderCostCompatibility {
pub session_model: fn(&serde_json::Value) -> Option<String>,
pub estimator: kcode_chatend::ProviderCostEstimator,
}
pub struct ReadModel {
pub provider_cost_compatibility: Option<ProviderCostCompatibility>,
}
impl ReadModel {
pub fn prepare_control_state(&self, state: &mut serde_json::Value);
pub fn active_summary(&self, record: SessionRecord) -> SessionRecord;
pub fn active(&self, record: SessionRecord, log: &SessionLog) -> SessionRecord;
pub fn completed(
&self,
receipt: CompletionReceipt,
summary: bool,
) -> SessionRecord;
pub fn legacy_provider_cost_summary_for_archive(
&self,
archive: &serde_json::Value,
session_state: Option<&serde_json::Value>,
) -> anyhow::Result<Option<kcode_chatend::ProviderCostSummary>>;
}
```
`prepare_control_state` preserves the existing first-user-message summary of at
most 512 Unicode scalar values so control-only enumeration remains bounded.
`active_summary` emits only the established summary fields and marks the record
as a summary. `active` materializes transcript and raw events from the complete
session log when absent. When valid Chatend metadata is present it also
reconstructs boxes and context, and projects the newest submitted provider
input or reconstructed text into the established fields, including an active
`historyIngress`. Replay failure leaves those exact Chatend fields absent.
`completed` projects a receipt as the established complete `SessionRecord`.
Legacy provider-cost projection is disabled when compatibility is absent and
returns no summary for a valid metadata-free session-log archive.
Projection work is O(events, boxes, and rendered context), with output and peak
allocation proportional to the complete projected data. It performs no disk or
network I/O, retries, waits, truncation, or caller-context limits. The caller
supplies already-read complete records, logs, and archives and owns any later
presentation truncation.