polyc-facts 2026.8.3

Shared semantic-fold library: decode-to-fact functions reused by every consumer that reads the event log, so a payment receipt or a tool call means the same thing everywhere it's read.
//! Shared semantic-fold library: pure decode-to-fact functions.
//!
//! The event log already has one canonical bytes→struct decode path
//! ([`polyc_proto::events_decode`]); what was missing was one canonical
//! **fold** on top of an already-decoded struct — the step that turns a
//! decoded wire message or a signed receipt into a fact with meaning ("this
//! is a tool call", "this receipt is verified"). Before this crate existed,
//! that fold was reimplemented independently by every consumer that wanted
//! it, and the reimplementations could (and did) disagree — see
//! `docs/reference/datafusion-fact-model.md` for the full account, including a
//! payment-receipt divergence this crate closes.
//!
//! This crate owns exactly the fold: mechanical, keyed by a stable id,
//! wanted in the same shape by two or more consumers. It has no I/O and no
//! async: every function here is a pure, synchronous transform so a caller
//! that can never fail open (a spend gate, a budget check) can call it
//! directly without taking on a query engine's availability as a dependency.
//!
//! Presentation stays with the consumer: truncation, snippet windowing,
//! ordering, and size budgets are per-surface shaping this crate never does.
//! Facts come out whole.
//!
//! # Two amendments to that charter, and why
//!
//! **Folds may decode.** An earlier wording said callers pass already-decoded
//! structs. That was never true of the code — most entry points here take a
//! raw `payload: &[u8]` or an `&Event` and decode internally, and
//! [`attribution_events_with_positions`] takes positioned events — so the
//! sentence described an intention the crate had already outgrown. Decoding
//! belongs with the fold that gives the bytes meaning.
//!
//! **One projection lives here: commit scoping.** [`committed_turn_ids`] and
//! [`committed_message_facts`] decide which turns are searchable and extract
//! their text. That is a policy, and [`attribution_events`]' own doc still
//! correctly says commit scoping "stays with the caller" for ITS purposes,
//! where consumers legitimately disagree about what committed means. The
//! narrow exception is for consumers that must NOT disagree: history
//! navigation and participation-scoped search answer the same question, so a
//! second implementation is a bug waiting for the two to drift
//! (`docs/proposals/participation-scoped-agent-search.md`). See
//! [`committed_message_facts`]'s own doc for the boundary.

#![forbid(unsafe_code)]
#![deny(missing_docs)]

mod approvals;
mod attribution;
mod committed;
mod excision;
mod grant_replays;
mod handoffs;
mod incognito;
mod message_content;
mod model_call;
mod receipts;
mod refusals;
mod subagent;
mod usage;
mod wallet_link_lifecycle;
mod withholding;

pub use approvals::{
    ApprovalFact, ApprovalRequestFact, ApprovalResponseFact, ApprovalSignatureStatus,
    classify_response_signature, fold_approval_event,
};
pub use attribution::{
    AttributionEventKind, AttributionFact, AttributionScope, attribution_events,
    attribution_events_with_positions, caller_by_turn_last_wins,
};
pub use committed::{CommittedMessageFact, committed_message_facts, committed_turn_ids};
pub use excision::{
    excised_positions, strip_excised, verified_excisions, verified_excisions_matching,
};
pub use grant_replays::{GrantReplayFact, GrantReplaySignatureStatus, fold_grant_replay_event};
pub use handoffs::{HandoffDeniedFact, HandoffFact, HandoffSpawnFact, fold_handoff_event};
pub use incognito::fold_incognito_set;
pub use message_content::{
    MessageContent, MessageContentFold, TextFact, ToolCallFact, ToolResultFact,
    fold_message_content,
};
pub use model_call::{DecodeParamsFact, ModelCallFact, fold_model_call_event};
pub use receipts::{verified_outbound_receipts, verified_receipts};
pub use refusals::verified_refusals;
pub use subagent::{
    SubagentResultFact, SubagentSpawnFact, fold_subagent_model_call, fold_subagent_result,
    fold_subagent_spawn,
};
pub use usage::{UsageFact, fold_usage_event};
pub use wallet_link_lifecycle::verified_wallet_link_lifecycle_events;
pub use withholding::{
    withheld_turn_ids, withheld_turn_ids_positioned, withhold_paused_turn_text,
    withhold_paused_turn_text_positioned,
};