# K1 groups projection
This library composes the K1 groups domain, recovery, SQLite, and state leaves into one durable projection handle. The domain leaf owns public values, validation, and sentinel meaning. The state leaf owns authorization, deterministic outcomes, reservations, queries, and receipt-checked publication. The SQLite leaf owns current-format serialization and transactions. The recovery leaf owns root and database selection, canonical KTO validation, and recoverable rebuild. This facade owns only operation locking, availability, and the open warning. It owns no KTO registration, callback execution, SQL, state/index logic, migration, compatibility reader, retry, worker, or generic framework.
## Public API
```rust
use std::path::Path;
use kcode_k1_txn_ordering::K1TxnOrdering;
pub use kcode_k1_groups_domain::{
ApplyOutcome, Group, GroupAction, GroupId, GroupMemberships, GroupName, GroupRevision,
GroupRole, GroupUser, ModelId, SentinelGroup, UserId, ALL_MODELS, ALL_USERS, LOCAL_MODELS,
};
pub use kcode_k1_txn_ordering::TxId;
pub const ALL_USERS: GroupId;
pub const ALL_MODELS: GroupId;
pub const LOCAL_MODELS: GroupId;
pub enum SentinelGroup {
AllUsers,
AllModels,
LocalModels,
}
pub struct GroupId { /* private fields */ }
pub struct ModelId { /* private fields */ }
pub struct GroupName { /* private fields */ }
pub enum GroupRole {
User,
Admin,
Owner,
}
impl GroupId {
pub const fn new(txid: TxId) -> Self;
pub const fn txid(self) -> TxId;
pub const fn sentinel(self) -> Option<SentinelGroup>;
}
impl ModelId {
pub const fn from_bytes(bytes: [u8; 32]) -> Self;
pub const fn as_bytes(&self) -> &[u8; 32];
pub const fn into_bytes(self) -> [u8; 32];
}
impl GroupName {
pub fn new(value: String) -> Result<Self, String>;
pub fn as_str(&self) -> &str;
pub fn into_string(self) -> String;
}
pub struct GroupUser { /* private fields */ }
pub struct GroupRevision { /* private fields */ }
pub struct Group { /* private fields */ }
pub struct GroupMemberships { /* private fields */ }
impl GroupUser {
pub fn user_id(&self) -> UserId;
pub fn role(&self) -> GroupRole;
}
impl GroupRevision {
pub fn group_id(&self) -> GroupId;
pub fn txid(&self) -> TxId;
}
impl Group {
pub fn id(&self) -> GroupId;
pub fn name(&self) -> &GroupName;
pub fn revision(&self) -> &GroupRevision;
pub fn users(&self) -> &[GroupUser];
pub fn models(&self) -> &[ModelId];
}
impl GroupMemberships {
pub fn revision(&self) -> Option<TxId>;
pub fn user_groups(&self) -> &[GroupId];
pub fn model_groups(&self) -> &[GroupId];
pub fn shared_groups(&self) -> &[GroupId];
}
pub enum GroupAction {
Create { owner: UserId, name: GroupName },
Rename { group: GroupId, actor: UserId, name: GroupName },
SetUserRole { group: GroupId, actor: UserId, user: UserId, role: Option<GroupRole> },
SetModelMembership { group: GroupId, actor: UserId, model: ModelId, present: bool },
}
pub enum ApplyOutcome {
Applied(GroupRevision),
Unchanged(GroupRevision),
Rejected(String),
}
pub struct Projection { /* private fields */ }
impl Projection {
pub fn open(root: &Path, ordering: &K1TxnOrdering) -> Result<(Self, Option<TxId>), String>;
pub fn apply(&self, callback_txid: TxId, action: GroupAction) -> Result<ApplyOutcome, String>;
pub fn get(&self, group: GroupId) -> Result<Option<Group>, String>;
pub fn groups_for_user(&self, user: UserId) -> Result<Vec<GroupId>, String>;
pub fn groups_for_model(&self, model: ModelId) -> Result<Vec<GroupId>, String>;
pub fn memberships(&self, user: UserId, model: ModelId) -> Result<GroupMemberships, String>;
pub fn clear(&self) -> Result<(), String>;
}
```
`SentinelGroup`, `GroupId`, `ModelId`, and `GroupRole` implement `Copy`, `Clone`, `Debug`, `Eq`, `Hash`, `Ord`, `PartialEq`, and `PartialOrd`. `GroupName` implements the same traits except `Copy`. `GroupUser`, `GroupRevision`, `Group`, `GroupMemberships`, `GroupAction`, and `ApplyOutcome` implement `Clone`, `Debug`, `Eq`, and `PartialEq`. `Projection` is `Send + Sync`. All fields are private. `UserId` and `TxId` are the exact dependency types; relevant identity operations are `UserId::from_tx_id`, `UserId::as_tx_id`, `TxId::from_bytes`, and `TxId::as_bytes`.
`GroupName::new` accepts exact UTF-8 values of 1 through 128 bytes, rejects control characters, and requires a non-whitespace character. It validates in that order and returns the domain leaf's exact error text. It never trims, normalizes, case-folds, or changes accepted text. Names need not be unique.
The three sentinel constants have transaction bytes ending in `1`, `2`, and `3` after the common prefix `[255, 75, 49, 71, 82, 80, 0, 0, 0, 0, 0]`. They are synthetic, immutable, absent from storage, and recognized exactly by `GroupId::sentinel`. `groups_for_user` always adds `ALL_USERS`; `groups_for_model` always adds `ALL_MODELS`; `memberships` adds each only to its matching set and computes shared groups only from ordinary membership. `LOCAL_MODELS` has no members. `get` returns `None` for sentinels. Result order is unspecified.
A group has one role per human and at least one Owner. Admins may add or remove Users but cannot affect Admins or Owners or assign those roles. Owners may make every human transition except removing or demoting the final Owner, and only Owners may rename or change model membership. Users and absent actors cannot mutate. The state leaf preserves the exact deterministic rejection precedence and text. Authorized no-op changes return `Unchanged`; semantic failures return `Rejected`; applied changes return `Applied` and set the addressed group revision to the callback transaction ID.
`apply` treats a Create callback transaction ID as the new group ID. Reserved Creates and all sentinel mutations are rejected. Every successful call, including `Rejected` and `Unchanged`, durably advances the callback cursor, while only `Applied` changes a group revision. The caller supplies canonical callback IDs assigned to `k1-groups-subsystem`; this facade performs no per-apply KTO validation. All query results own their data, and `memberships` returns one coherent cursor and membership snapshot.
## Persistence, recovery, and availability
`open` delegates to the recovery leaf, returns its state cursor, and uses exactly `root/groups.sqlite3` with SQLite WAL and synchronous `FULL`. Current schema validation, KTO reference validation, and recoverable rebuild are leaf-owned. Recoverable derived-state failures remove the WAL, SHM, and database in that order and recreate empty state. Operational, filesystem, SQLite, and KTO query failures are fatal and non-destructive. There is no migration or compatibility reader.
A complete open taking strictly more than 100 milliseconds emits exactly one stderr line `level=warn module=kcode-k1-groups-projection operation=open elapsed_us=<integer> outcome=<ready|error>`. Faster opens emit nothing.
One apply lane serializes `apply` and `clear`. Apply prepares under the state write lock, releases it for one SQLite commit, then publishes the receipt-verified prepared change under the state write lock. Clear commits storage first and then clears state with its receipt. Queries use only the state read lock and remain available on the prior coherent snapshot during SQLite work. No callback or KTO query runs under these locks. Distinct handles have no package-global coordination.
Store commit failure, clear persistence failure, or post-commit state publication failure makes the handle unavailable until reopen and preserves the leaf error. Lock poisoning first returns exactly `projection apply lane is unavailable until reopen`, `projection database lane is unavailable until reopen`, or `projection state is unavailable until reopen` for the affected lock and also marks the handle unavailable. Later operations return `projection is unavailable until reopen`.
All operations are synchronous. The library starts no thread and has no queue, network request, timeout, or retry. A SQLite mutation may wait up to five seconds for database lock contention. Queries perform no disk I/O after open.
## Work and performance
For `G` groups, `U` human rows, `M` model rows, an addressed group with `u` humans and `m` models, and result size `R`, open uses `O(G + U + M)` work and memory plus recovery's distinct canonical KTO lookups. Apply uses expected `O(1)` state work and constant indexed SQLite work. Get uses expected `O(1) + O(u + m)` work. Reverse queries use expected `O(1) + O(R)` work, and shared membership intersects the smaller ordinary reverse set. Clear uses `O(G + U + M)` deletion and destruction work. There are no cardinality caps.
Filesystem, allocator, scheduler, SQLite, and KTO latency have no general finite bound. The external groups projection testkits and their concrete adapter own behavior, recovery, blocked-mutation isolation, and scale acceptance in the hardened rootless Podman managed-check environment. The scale fixture uses 10,000 groups, 100,000 human memberships, and 100,000 model memberships; each of three valid opens and one recoverable rebuild must finish within five seconds, and each of five samples per query operation must finish within 200 milliseconds while returning complete results.