shiplog 0.8.0

CLI evidence compiler for review-cycle packets with receipts, coverage, gaps, and safe share profiles.
Documentation
use super::claims::ClaimTracker;
use super::stats;
use super::{LlmWorkstream, MAX_RECEIPTS_PER_WORKSTREAM};
use shiplog::ids::WorkstreamId;
use shiplog::schema::event::EventEnvelope;
use shiplog::schema::workstream::Workstream;

pub(super) fn build_claimed_workstreams(
    llm_workstreams: Vec<LlmWorkstream>,
    events: &[EventEnvelope],
    claims: &mut ClaimTracker,
) -> Vec<Workstream> {
    llm_workstreams
        .into_iter()
        .enumerate()
        .filter_map(|(index, llm_workstream)| {
            build_claimed_workstream(index, llm_workstream, events, claims)
        })
        .collect()
}

pub(super) fn build_uncategorized_workstream(
    events: &[EventEnvelope],
    orphan_indices: &[usize],
) -> Option<Workstream> {
    if orphan_indices.is_empty() {
        return None;
    }

    let selection = stats::summarize_events(events, orphan_indices);
    let receipts = stats::receipt_ids_for_indices(
        events,
        orphan_indices.iter().copied(),
        MAX_RECEIPTS_PER_WORKSTREAM,
    );

    Some(Workstream {
        id: WorkstreamId::from_parts(["llm", "uncategorized"]),
        title: "Uncategorized".to_string(),
        summary: Some("Events not assigned to any thematic workstream".to_string()),
        tags: vec!["uncategorized".to_string()],
        stats: selection.stats,
        events: selection.event_ids,
        receipts,
    })
}

fn build_claimed_workstream(
    workstream_index: usize,
    llm_workstream: LlmWorkstream,
    events: &[EventEnvelope],
    claims: &mut ClaimTracker,
) -> Option<Workstream> {
    let valid_event_indices = claims.claim_available_indices(llm_workstream.event_indices);
    if valid_event_indices.is_empty() {
        return None;
    }

    let valid_receipt_indices = claimed_receipt_indices(
        llm_workstream.receipt_indices,
        &valid_event_indices,
        MAX_RECEIPTS_PER_WORKSTREAM,
    );
    let selection = stats::summarize_events(events, &valid_event_indices);
    let receipt_ids = stats::receipt_ids_for_indices(events, valid_receipt_indices, usize::MAX);

    Some(Workstream {
        id: WorkstreamId::from_parts(["llm", &workstream_index.to_string()]),
        title: llm_workstream.title,
        summary: llm_workstream.summary,
        tags: llm_workstream.tags,
        stats: selection.stats,
        events: selection.event_ids,
        receipts: receipt_ids,
    })
}

fn claimed_receipt_indices(
    receipt_indices: Vec<usize>,
    valid_event_indices: &[usize],
    limit: usize,
) -> Vec<usize> {
    receipt_indices
        .into_iter()
        .filter(|index| valid_event_indices.contains(index))
        .take(limit)
        .collect()
}