logline_core/
lib.rs

1//! # logline-core
2//!
3//! The Conceptual Atom of Verifiable Action — Paper I §3 (9-field tuple, lifecycle, invariants, Ghost Records)
4//!
5//! See [README.md](https://github.com/LogLine-Foundation/logline-core/blob/main/README.md) for full documentation.
6
7#![cfg_attr(docsrs, feature(doc_cfg))]
8#![cfg_attr(not(feature = "std"), no_std)]
9#![forbid(unsafe_code)]
10
11#[cfg(not(feature = "std"))]
12extern crate alloc;
13
14#[cfg(not(feature = "std"))]
15use alloc::{string::String, vec::Vec};
16#[cfg(feature = "std")]
17use std::{string::String, vec::Vec};
18
19mod builder;
20mod consequence;
21mod ghost;
22mod payload;
23mod signature;
24mod status;
25mod verb;
26
27pub use builder::LogLineBuilder;
28pub use consequence::{Escalation, FailureHandling, Outcome};
29pub use ghost::GhostRecord;
30pub use payload::Payload;
31pub use signature::{SignError, Signable, Signature, Signer};
32pub use status::Status;
33pub use verb::{Verb, VerbRegistry};
34
35use thiserror::Error;
36
37/// Número rígido de campos do átomo conceitual (Paper I §3).
38pub const TUPLE_FIELD_COUNT: usize = 9;
39
40/// Erros que podem ocorrer ao manipular um `LogLine`.
41///
42/// Todos os erros são retornados como `LogLineError` e podem ser convertidos
43/// em strings legíveis usando `Display` (via `thiserror`).
44#[derive(Error, Debug, PartialEq)]
45pub enum LogLineError {
46    /// Um invariant obrigatório está faltando ou vazio.
47    ///
48    /// Os invariants obrigatórios são: `if_ok`, `if_doubt`, `if_not`.
49    #[error("Missing consequence invariant: {0}")]
50    MissingInvariant(&'static str),
51    /// Um campo obrigatório está faltando.
52    ///
53    /// Campos obrigatórios: `who`, `did`, `when`.
54    #[error("Missing field: {0}")]
55    MissingField(&'static str),
56    /// Tentativa de transição de status inválida.
57    ///
58    /// O lifecycle permite apenas: `DRAFT → PENDING → COMMITTED` ou `DRAFT/PENDING → GHOST`.
59    #[error("Invalid status transition: {from:?} → {to:?}")]
60    InvalidTransition { from: Status, to: Status },
61    /// Tentativa de abandonar um LogLine que já está `Committed`.
62    ///
63    /// LogLines `Committed` são imutáveis e não podem ser abandonados.
64    #[error("Cannot ghost a committed LogLine")]
65    AlreadyCommitted,
66    /// Erro durante a assinatura do LogLine.
67    #[error("Signing error")]
68    Signing,
69}
70
71/// O "átomo" LogLine — 9-field tuple rígido.
72///
73/// Representa uma ação verificável com lifecycle determinístico e invariants obrigatórios.
74/// Conforme Paper I §3, cada LogLine deve ter exatamente 9 campos e seguir o lifecycle
75/// `DRAFT → PENDING → COMMITTED | GHOST`.
76///
77/// # Exemplo
78///
79/// ```rust
80/// use logline_core::*;
81///
82/// let line = LogLine::builder()
83///     .who("did:ubl:alice")
84///     .did(Verb::Approve)
85///     .this(Payload::Text("purchase:123".into()))
86///     .when(1_735_671_234)
87///     .if_ok(Outcome { label: "approved".into(), effects: vec!["emit_receipt".into()] })
88///     .if_doubt(Escalation { label: "manual_review".into(), route_to: "auditor".into() })
89///     .if_not(FailureHandling { label: "rejected".into(), action: "notify".into() })
90///     .build_draft()?;
91/// # Ok::<(), LogLineError>(())
92/// ```
93#[derive(Clone, Debug, PartialEq)]
94#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
95pub struct LogLine {
96    /// Identidade do agente que executa a ação (DID futuro, ex: `did:ubl:...`).
97    pub who: String,
98    /// Verbo canônico ou custom (Paper I: validar contra ALLOWED_ACTIONS via VerbRegistry).
99    pub did: Verb,
100    /// Carga útil mínima/typed (Paper I: JSON estrito validado por schema do verbo).
101    pub this: Payload,
102    /// Unix timestamp em nanosegundos (interno). Na serialização canônica JSON✯Atomic será ISO8601.
103    pub when: u64,
104    /// Identidade que confirma a ação. Paper I: obrigatório para ações de Risk Level 3+ (L3+).
105    pub confirmed_by: Option<String>,
106    /// Consequência positiva obrigatória (Paper I: invariante obrigatório).
107    pub if_ok: Outcome,
108    /// Via de dúvida obrigatória (Paper I: invariante obrigatório).
109    pub if_doubt: Escalation,
110    /// Fallback/erro obrigatório (Paper I: invariante obrigatório).
111    pub if_not: FailureHandling,
112    /// Estado do lifecycle rígido: `DRAFT → PENDING → COMMITTED | GHOST`.
113    pub status: Status,
114    // pub signature: Option<Signature> // (futuro)
115}
116
117impl LogLine {
118    /// Cria um novo builder para construir um `LogLine` passo a passo.
119    ///
120    /// # Exemplo
121    ///
122    /// ```rust
123    /// use logline_core::*;
124    ///
125    /// let builder = LogLine::builder()
126    ///     .who("did:ubl:alice")
127    ///     .did(Verb::Transfer);
128    /// ```
129    pub fn builder() -> LogLineBuilder {
130        LogLineBuilder::new()
131    }
132
133    /// Verifica invariants do 9-tuple (Paper I §3).
134    ///
135    /// Valida que todos os campos obrigatórios estão presentes e não vazios:
136    /// - `who` não pode ser vazio
137    /// - `when` deve ser > 0
138    /// - `if_ok`, `if_doubt`, `if_not` devem estar presentes e não vazios
139    ///
140    /// # Erros
141    ///
142    /// Retorna `LogLineError::MissingField` ou `LogLineError::MissingInvariant`
143    /// se algum invariant não for satisfeito.
144    ///
145    /// # Exemplo
146    ///
147    /// ```rust
148    /// use logline_core::*;
149    ///
150    /// let line = LogLine::builder()
151    ///     .who("did:ubl:alice")
152    ///     .did(Verb::Approve)
153    ///     .when(1_735_671_234)
154    ///     .if_ok(Outcome { label: "ok".into(), effects: vec![] })
155    ///     .if_doubt(Escalation { label: "doubt".into(), route_to: "auditor".into() })
156    ///     .if_not(FailureHandling { label: "not".into(), action: "notify".into() })
157    ///     .build_draft()?;
158    ///
159    /// assert!(line.verify_invariants().is_ok());
160    /// # Ok::<(), LogLineError>(())
161    /// ```
162    pub fn verify_invariants(&self) -> Result<(), LogLineError> {
163        if self.who.is_empty() {
164            return Err(LogLineError::MissingField("who"));
165        }
166        if self.when == 0 {
167            return Err(LogLineError::MissingField("when"));
168        }
169        if self.if_ok.is_empty() {
170            return Err(LogLineError::MissingInvariant("if_ok"));
171        }
172        if self.if_doubt.is_empty() {
173            return Err(LogLineError::MissingInvariant("if_doubt"));
174        }
175        if self.if_not.is_empty() {
176            return Err(LogLineError::MissingInvariant("if_not"));
177        }
178        Ok(())
179    }
180
181    /// Assina o LogLine (DRAFT ou PENDING). Paper I: "nada acontece sem estar assinado".
182    ///
183    /// A assinatura é calculada sobre os bytes determinísticos retornados por
184    /// `to_signable_bytes()`. Retorna `self` para method chaining.
185    ///
186    /// # Erros
187    ///
188    /// - `LogLineError::InvalidTransition` se o status não for `Draft` ou `Pending`
189    /// - `LogLineError::Signing` se a assinatura falhar
190    ///
191    /// # Exemplo
192    ///
193    /// ```rust
194    /// use logline_core::*;
195    ///
196    /// struct NoopSigner;
197    /// impl Signer for NoopSigner {
198    ///     fn sign(&self, _msg: &[u8]) -> Result<Signature, SignError> {
199    ///         Ok(Signature { alg: "none".into(), bytes: vec![] })
200    ///     }
201    /// }
202    ///
203    /// let signer = NoopSigner;
204    /// let line = LogLine::builder()
205    ///     .who("did:ubl:alice")
206    ///     .did(Verb::Approve)
207    ///     .when(1_735_671_234)
208    ///     .if_ok(Outcome { label: "ok".into(), effects: vec![] })
209    ///     .if_doubt(Escalation { label: "doubt".into(), route_to: "auditor".into() })
210    ///     .if_not(FailureHandling { label: "not".into(), action: "notify".into() })
211    ///     .build_draft()?
212    ///     .sign(&signer)?;
213    /// # Ok::<(), LogLineError>(())
214    /// ```
215    pub fn sign(self, signer: &dyn Signer) -> Result<Self, LogLineError> {
216        match self.status {
217            Status::Draft | Status::Pending => {
218                let _sig = signer
219                    .sign(&self.to_signable_bytes())
220                    .map_err(|_| LogLineError::Signing)?;
221                // self.signature = Some(sig); // quando ativarmos
222                Ok(self)
223            }
224            _ => Err(LogLineError::InvalidTransition {
225                from: self.status,
226                to: Status::Committed,
227            }),
228        }
229    }
230
231    /// Congela o DRAFT em PENDING (pronto para sign/commit/ghost).
232    ///
233    /// Valida os invariants antes de fazer a transição. Após `freeze()`, o LogLine
234    /// está pronto para ser assinado e commitado, ou abandonado como Ghost.
235    ///
236    /// # Erros
237    ///
238    /// - `LogLineError::InvalidTransition` se o status não for `Draft`
239    /// - `LogLineError::MissingField` ou `LogLineError::MissingInvariant` se os invariants falharem
240    ///
241    /// # Exemplo
242    ///
243    /// ```rust
244    /// use logline_core::*;
245    ///
246    /// let line = LogLine::builder()
247    ///     .who("did:ubl:alice")
248    ///     .did(Verb::Approve)
249    ///     .when(1_735_671_234)
250    ///     .if_ok(Outcome { label: "ok".into(), effects: vec![] })
251    ///     .if_doubt(Escalation { label: "doubt".into(), route_to: "auditor".into() })
252    ///     .if_not(FailureHandling { label: "not".into(), action: "notify".into() })
253    ///     .build_draft()?
254    ///     .freeze()?;
255    ///
256    /// assert_eq!(line.status, Status::Pending);
257    /// # Ok::<(), LogLineError>(())
258    /// ```
259    pub fn freeze(mut self) -> Result<Self, LogLineError> {
260        status::ensure(Status::Draft, Status::Pending, self.status)?;
261        self.verify_invariants()?;
262        self.status = Status::Pending;
263        Ok(self)
264    }
265
266    /// Congela com validação de verbo contra ALLOWED_ACTIONS (Paper I: verbo deve estar no registro).
267    ///
268    /// Equivalente a `freeze()`, mas valida primeiro se o verbo (`did`) está permitido
269    /// no sistema através do `VerbRegistry`. Útil para implementar políticas de segurança
270    /// onde apenas verbos específicos são permitidos.
271    ///
272    /// # Erros
273    ///
274    /// - `LogLineError::MissingField("did (unknown verb)")` se o verbo não estiver no registro
275    /// - Erros de `freeze()` se os invariants falharem
276    ///
277    /// # Exemplo
278    ///
279    /// ```rust
280    /// use logline_core::*;
281    ///
282    /// struct SimpleRegistry;
283    /// impl VerbRegistry for SimpleRegistry {
284    ///     fn is_allowed(&self, verb: &Verb) -> bool {
285    ///         matches!(verb, Verb::Transfer | Verb::Approve)
286    ///     }
287    /// }
288    ///
289    /// let registry = SimpleRegistry;
290    /// let line = LogLine::builder()
291    ///     .who("did:ubl:alice")
292    ///     .did(Verb::Approve)
293    ///     .when(1_735_671_234)
294    ///     .if_ok(Outcome { label: "ok".into(), effects: vec![] })
295    ///     .if_doubt(Escalation { label: "doubt".into(), route_to: "auditor".into() })
296    ///     .if_not(FailureHandling { label: "not".into(), action: "notify".into() })
297    ///     .build_draft()?
298    ///     .freeze_with_registry(&registry)?;
299    /// # Ok::<(), LogLineError>(())
300    /// ```
301    pub fn freeze_with_registry(self, registry: &dyn VerbRegistry) -> Result<Self, LogLineError> {
302        if !registry.is_allowed(&self.did) {
303            return Err(LogLineError::MissingField("did (unknown verb)"));
304        }
305        self.freeze()
306    }
307
308    /// Commit final (PENDING → COMMITTED). Paper I: requer assinatura obrigatória.
309    ///
310    /// Transiciona o LogLine de `Pending` para `Committed`, assinando os bytes determinísticos.
311    /// Uma vez `Committed`, o LogLine não pode mais ser modificado ou abandonado.
312    ///
313    /// # Erros
314    ///
315    /// - `LogLineError::InvalidTransition` se o status não for `Pending`
316    /// - `LogLineError::Signing` se a assinatura falhar
317    ///
318    /// # Exemplo
319    ///
320    /// ```rust
321    /// use logline_core::*;
322    ///
323    /// struct NoopSigner;
324    /// impl Signer for NoopSigner {
325    ///     fn sign(&self, _msg: &[u8]) -> Result<Signature, SignError> {
326    ///         Ok(Signature { alg: "none".into(), bytes: vec![] })
327    ///     }
328    /// }
329    ///
330    /// let signer = NoopSigner;
331    /// let line = LogLine::builder()
332    ///     .who("did:ubl:alice")
333    ///     .did(Verb::Approve)
334    ///     .when(1_735_671_234)
335    ///     .if_ok(Outcome { label: "ok".into(), effects: vec![] })
336    ///     .if_doubt(Escalation { label: "doubt".into(), route_to: "auditor".into() })
337    ///     .if_not(FailureHandling { label: "not".into(), action: "notify".into() })
338    ///     .build_draft()?
339    ///     .freeze()?
340    ///     .commit(&signer)?;
341    ///
342    /// assert_eq!(line.status, Status::Committed);
343    /// # Ok::<(), LogLineError>(())
344    /// ```
345    pub fn commit(mut self, signer: &dyn Signer) -> Result<Self, LogLineError> {
346        status::ensure(Status::Pending, Status::Committed, self.status)?;
347        // Futuro: bytes canônicos JSON✯Atomic; por ora, ordem estável de campos:
348        let _sig = signer
349            .sign(&self.to_signable_bytes())
350            .map_err(|_| LogLineError::Signing)?;
351        // self.signature = Some(sig); // quando ativarmos
352        self.status = Status::Committed;
353        Ok(self)
354    }
355
356    /// Abandona intenção: DRAFT/PENDING → GHOST (forensics).
357    ///
358    /// Versão sem assinatura (compatibilidade). Para versão assinada, use `abandon_signed()`.
359    /// Cria um `GhostRecord` que preserva o LogLine original para análise forense.
360    ///
361    /// # Erros
362    ///
363    /// - `LogLineError::AlreadyCommitted` se o LogLine já estiver `Committed`
364    ///
365    /// # Exemplo
366    ///
367    /// ```rust
368    /// use logline_core::*;
369    ///
370    /// let line = LogLine::builder()
371    ///     .who("did:ubl:alice")
372    ///     .did(Verb::Deploy)
373    ///     .when(1_735_671_234)
374    ///     .if_ok(Outcome { label: "ok".into(), effects: vec![] })
375    ///     .if_doubt(Escalation { label: "doubt".into(), route_to: "qa".into() })
376    ///     .if_not(FailureHandling { label: "not".into(), action: "rollback".into() })
377    ///     .build_draft()?;
378    ///
379    /// let ghost = line.abandon(Some("user_cancelled".into()))?;
380    /// assert_eq!(ghost.status, Status::Ghost);
381    /// # Ok::<(), LogLineError>(())
382    /// ```
383    pub fn abandon(self, reason: Option<String>) -> Result<GhostRecord, LogLineError> {
384        match self.status {
385            Status::Committed => Err(LogLineError::AlreadyCommitted),
386            Status::Draft | Status::Pending => Ok(GhostRecord::from(self, reason)),
387            _ => Ok(GhostRecord::from(self, reason)),
388        }
389    }
390
391    /// Abandona intenção assinada: DRAFT/PENDING → GHOST (forensics).
392    ///
393    /// Paper I: attempt já nasce assinado, então o abandon também deve ser assinado.
394    /// Versão recomendada que assina o LogLine antes de criar o GhostRecord.
395    ///
396    /// # Erros
397    ///
398    /// - `LogLineError::AlreadyCommitted` se o LogLine já estiver `Committed`
399    /// - `LogLineError::Signing` se a assinatura falhar
400    ///
401    /// # Exemplo
402    ///
403    /// ```rust
404    /// use logline_core::*;
405    ///
406    /// struct NoopSigner;
407    /// impl Signer for NoopSigner {
408    ///     fn sign(&self, _msg: &[u8]) -> Result<Signature, SignError> {
409    ///         Ok(Signature { alg: "none".into(), bytes: vec![] })
410    ///     }
411    /// }
412    ///
413    /// let signer = NoopSigner;
414    /// let line = LogLine::builder()
415    ///     .who("did:ubl:alice")
416    ///     .did(Verb::Deploy)
417    ///     .when(1_735_671_234)
418    ///     .if_ok(Outcome { label: "ok".into(), effects: vec![] })
419    ///     .if_doubt(Escalation { label: "doubt".into(), route_to: "qa".into() })
420    ///     .if_not(FailureHandling { label: "not".into(), action: "rollback".into() })
421    ///     .build_draft()?;
422    ///
423    /// let ghost = line.abandon_signed(&signer, Some("timeout".into()))?;
424    /// assert_eq!(ghost.status, Status::Ghost);
425    /// # Ok::<(), LogLineError>(())
426    /// ```
427    pub fn abandon_signed(
428        self,
429        signer: &dyn Signer,
430        reason: Option<String>,
431    ) -> Result<GhostRecord, LogLineError> {
432        match self.status {
433            Status::Committed => Err(LogLineError::AlreadyCommitted),
434            Status::Draft | Status::Pending => {
435                let _sig = signer
436                    .sign(&self.to_signable_bytes())
437                    .map_err(|_| LogLineError::Signing)?;
438                Ok(GhostRecord::from(self, reason))
439            }
440            _ => Ok(GhostRecord::from(self, reason)),
441        }
442    }
443
444    /// Bytes determinísticos "suficientes" para v0.1 (placeholder).
445    ///
446    /// Gera uma representação determinística dos campos principais do LogLine
447    /// para assinatura. Em versões futuras, isso será substituído por bytes canônicos
448    /// JSON✯Atomic (via `json_atomic`).
449    ///
450    /// Formato atual: `who|verb|when|status|confirmed_by|this.kind`
451    ///
452    /// # Exemplo
453    ///
454    /// ```rust
455    /// use logline_core::*;
456    ///
457    /// let line = LogLine::builder()
458    ///     .who("did:ubl:alice")
459    ///     .did(Verb::Approve)
460    ///     .when(1_735_671_234)
461    ///     .if_ok(Outcome { label: "ok".into(), effects: vec![] })
462    ///     .if_doubt(Escalation { label: "doubt".into(), route_to: "auditor".into() })
463    ///     .if_not(FailureHandling { label: "not".into(), action: "notify".into() })
464    ///     .build_draft()?;
465    ///
466    /// let bytes = line.to_signable_bytes();
467    /// assert!(!bytes.is_empty());
468    /// # Ok::<(), LogLineError>(())
469    /// ```
470    pub fn to_signable_bytes(&self) -> Vec<u8> {
471        // Ordem fixa: who|verb|when|status|confirmed_by|this.kind
472        let mut out = Vec::new();
473        out.extend_from_slice(self.who.as_bytes());
474        out.extend_from_slice(b"|");
475        out.extend_from_slice(self.did.as_str().as_bytes());
476        out.extend_from_slice(b"|");
477        out.extend_from_slice(self.when.to_string().as_bytes());
478        out.extend_from_slice(b"|");
479        out.extend_from_slice(self.status.as_str().as_bytes());
480        out.extend_from_slice(b"|");
481        if let Some(c) = &self.confirmed_by {
482            out.extend_from_slice(c.as_bytes());
483        }
484        out.extend_from_slice(b"|");
485        out.extend_from_slice(self.this.kind().as_bytes());
486        out
487    }
488}