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.
docs.rs failed to build polyc-facts-2026.8.3
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

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.