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(®istry)?;
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}