kcode-k1-groups-projection 0.1.0

Durable derived projection and authorization semantics for K1 groups
Documentation
# K1 groups projection

`kcode-k1-groups-projection` owns group authorization semantics and one local derived projection for the exact logical K1 subsystem `k1-groups-subsystem`. It owns no KTO registration or replay, Peering, HTTP or authentication, Access Controller, facade, model resolution, provider, worker, background task, polling, timeout, retry, names, nesting, deletion, metadata, or sentinel behavior.

## Public API

```rust
pub use kcode_k1_invites::UserId;
pub use kcode_k1_txn_ordering::TxId;

pub struct GroupId(TxId);
pub struct ModelId([u8; 32]);
pub enum GroupRole { User, Admin, Owner }

impl GroupId {
    pub const fn new(txid: TxId) -> Self;
    pub const fn txid(self) -> TxId;
}
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];
}

pub struct GroupUser;
impl GroupUser {
    pub fn user_id(&self) -> UserId;
    pub fn role(&self) -> GroupRole;
}

pub struct GroupRevision;
impl GroupRevision {
    pub fn group_id(&self) -> GroupId;
    pub fn txid(&self) -> TxId;
}

pub struct Group;
impl Group {
    pub fn id(&self) -> GroupId;
    pub fn revision(&self) -> &GroupRevision;
    pub fn users(&self) -> &[GroupUser];
    pub fn models(&self) -> &[ModelId];
}

pub struct GroupMemberships;
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 },
    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;
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>;
}
```

`UserId` is the exact public type from `kcode-k1-invites` 0.2.1, not a package-local wrapper. Its consumer constructors and accessors are `UserId::from_tx_id(TxId)` and `UserId::as_tx_id()`. Durable human membership identity is exactly `user_id.as_tx_id().as_bytes()`, totaling 12 bytes. `GroupId`, `ModelId`, and `GroupRole` are `Copy`, `Clone`, `Debug`, `Eq`, `Hash`, and ordered. `UserId` has the traits guaranteed by Invites. The owned result and action types are `Clone`, `Debug`, `Eq`, and `PartialEq`. Every `TxId`, invite `UserId`, and 32-byte model identity is in the local type domain. A valid `apply` callback ID is a canonical transaction assigned to `k1-groups-subsystem`, and a Create callback transaction ID is the new group ID.

A group has exactly one role per human, no role for models, and at least one Owner. An Admin may add an absent User, remove a User, or request the existing User role unchanged. An Admin cannot affect an Admin or Owner or assign either role. An Owner may perform every human transition except removing or demoting the final Owner. A User cannot mutate. Only an Owner changes model membership. Actor authority is established before an equal desired state can return `Unchanged`.

Unknown groups, insufficient authority, forbidden Admin transitions, final-owner conflicts, and duplicate Creates return deterministic `Rejected` outcomes. Every successful `apply` advances the global callback cursor in the same SQLite transaction, including `Rejected` and `Unchanged`. Only `Applied` changes the addressed group revision, which becomes that callback transaction ID. Query results own their data, their fields are private, and their listed accessors expose the complete result. `memberships` obtains its revision and all three sets from one coherent state snapshot. Result order is unspecified.

## Persistence and recovery

The root contains `groups.sqlite3` and its SQLite WAL/SHM sidecars. SQLite uses WAL and synchronous `FULL`. The normalized exact schema is singleton `metadata(schema_version, last_applied_txid)`, `groups(group_id, revision)`, `user_memberships(group_id, user_id, role)`, and `model_memberships(group_id, model_id)`, with composite membership primary keys, group references, and user/model reverse indexes. Group and transaction identifiers are 12-byte blobs, human identifiers are the 12 bytes of the canonical Invites `UserId`, and model identifiers are 32-byte opaque blobs. One `apply` transaction changes only its constant number of addressed rows and the cursor. `clear` deletes all rows and nulls the cursor in one durable transaction.

Open validates `quick_check`, the exact schema and schema version 1, byte widths, roles, references, uniqueness, owner invariants, group revisions, the cursor, and every stored group/revision/cursor transaction against KTO and `k1-groups-subsystem`. Recoverably corrupt, incompatible, malformed, noncanonical, absent-target, wrong-subsystem, or internally inconsistent derived state is removed with its sidecars and recreated empty. Malformed transaction bytes returned for an ID referenced only by derived rows are a recoverable derived-state inconsistency. KTO query errors and ordinary permission, busy, locked, read-only, full, cannot-open, locking, and filesystem failures are fatal and do not trigger deletion.

A complete open taking more than 100 ms emits exactly one data-free line in this format:

```text
level=warn module=kcode-k1-groups-projection operation=open elapsed_us=<integer> outcome=<ready|error>
```

Open builds group, user-to-group, and model-to-group hash indexes. One internal apply lane serializes mutations to this database. Semantics and required hash capacity are prepared under a short memory lock, SQLite commits without the memory lock, and an allocation-free delta is then published briefly. Queries therefore see either the prior or next coherent snapshot and remain usable on the prior snapshot during blocked SQLite work. No callback or KTO query occurs under a live projection lock. Persistence failure or an internal state contradiction makes the handle unavailable until reopen. The library starts no thread and has no queue, polling, retry, timeout, or caller-coordination protocol. Separate projection instances and unrelated KTO subsystem lanes have no package-global coordination.

## Work and performance contract

For `G` groups, `U` human membership rows, `M` model membership rows, one addressed group with `u` humans and `m` models, and output size `R`: open performs `O(G + U + M)` projection work and memory plus canonical KTO lookups for distinct stored group/revision/cursor IDs; `apply` performs average `O(1)` hash work, constant addressed SQLite statements, and no membership-sized copy; `get` performs average `O(1) + O(u + m)` work and allocates only its owned output; reverse queries perform average `O(1) + O(R)` work; shared membership intersects the smaller reverse set and never forms a cross-product; `clear` performs `O(G + U + M)` SQLite deletion and destruction work. Hash-table adversarial collision behavior, allocator delay, scheduler delay, SQLite, filesystem, and KTO latency have no finite general wall-clock guarantee. There are no arbitrary cardinality caps.

All calls are synchronous, make no network request, retry zero times, and have no internal timeout. Queries do no disk I/O after open. Apply and clear perform one local SQLite transaction. Allocation failures that can be surfaced are returned as strings before persistence.

The `scale` managed-check executable is the reproducible acceptance fixture in the hardened rootless Podman environment. It directly constructs 10,000 groups, 100,000 human memberships, and 100,000 model memberships over 10,000 canonical callback transactions. Each of three valid opens and one full-scan recoverable rebuild must complete in under five seconds. Five samples each of `get`, `groups_for_user`, `groups_for_model`, and `memberships` must complete in under 200 ms per call while returning their complete results. `many_to_many_reverse_intersection_and_revision` and `blocked_apply_does_not_block_queries` additionally verify smaller-set intersection, coherent cursor results, and slow-mutation isolation.