diff --git a/.github/workflows/native-core.yml b/.github/workflows/native-core.yml index 0373960..036dd47 100644 --- a/.github/workflows/native-core.yml +++ b/.github/workflows/native-core.yml @@ -8,6 +8,7 @@ on: paths: - Cargo.toml - Cargo.lock + - l64-symbolic/** - l64-native/** - l64-projection/** - l64-cli/** @@ -40,5 +41,5 @@ jobs: - name: Test workspace run: cargo test -q - - name: Lint native projection spine - run: cargo clippy -p l64-native -p l64-projection --all-targets -- -D warnings + - name: Lint native symbolic spine + run: cargo clippy -p l64-symbolic -p l64-native -p l64-projection --all-targets -- -D warnings diff --git a/Cargo.toml b/Cargo.toml index 90cf3c0..f476fb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "l64-native", + "l64-symbolic", "l64-projection", "l64-core", "l64-locus", diff --git a/LOCUS64_EXECUTION_COHERENCE_RAIL.athens b/LOCUS64_EXECUTION_COHERENCE_RAIL.athens index 0f31927..c0c4921 100644 --- a/LOCUS64_EXECUTION_COHERENCE_RAIL.athens +++ b/LOCUS64_EXECUTION_COHERENCE_RAIL.athens @@ -2,8 +2,11 @@ ATHENS_DEVELOPMENT_RAIL v1 field=key=current_stage;value=legacy-authority-quarantine field=key=next_stage;value=complete field=key=projection_authority;value=non_authoritative -field=key=rail_version;value=5 +field=key=rail_version;value=6 field=key=schema_version;value=1 +field=key=native_identity;value=exact_symbolic +field=key=native_compact_seal;value=non_authoritative +field=key=legacy_digest_boundary;value=blake3_l64_core_only gate=id=incremental-closure-green gate=id=legacy-authority-quarantine-green gate=id=native-constraint-core-green diff --git a/LOCUS64_SYMBOLIC_COMMITMENT_CHANGE_CHAIN.athens b/LOCUS64_SYMBOLIC_COMMITMENT_CHANGE_CHAIN.athens new file mode 100644 index 0000000..cd31424 --- /dev/null +++ b/LOCUS64_SYMBOLIC_COMMITMENT_CHANGE_CHAIN.athens @@ -0,0 +1,62 @@ +ATHENS_DEVELOPMENT_RAIL v1 +field=key=compact_seal_authority;value=non_authoritative +field=key=exact_identity_authority;value=domain_plus_canonical_bytes +field=key=legacy_blake3_boundary;value=l64-core-only +field=key=rail_version;value=2 +field=key=schema_version;value=1 +gate=id=exact-identity-envelope-green +gate=id=seal-coordinate-algebra-green +gate=id=composition-grammar-green +gate=id=native-section-symbol-green +gate=id=dna-v2-symbolic-frame-green +gate=id=projection-symbol-binding-green +gate=id=developer-surface-green +gate=id=symbolic-commitment-green +stage=id=exact-identity-envelope +stage_field=stage=exact-identity-envelope;key=required_gates;value=exact-identity-envelope-green +stage_field=stage=exact-identity-envelope;key=status;value=complete +stage=id=seal-coordinate-algebra +stage_field=stage=seal-coordinate-algebra;key=depends_on;value=exact-identity-envelope +stage_field=stage=seal-coordinate-algebra;key=required_gates;value=seal-coordinate-algebra-green +stage_field=stage=seal-coordinate-algebra;key=status;value=complete +stage=id=composition-grammar +stage_field=stage=composition-grammar;key=depends_on;value=seal-coordinate-algebra +stage_field=stage=composition-grammar;key=required_gates;value=composition-grammar-green +stage_field=stage=composition-grammar;key=status;value=complete +stage=id=native-section-symbol +stage_field=stage=native-section-symbol;key=depends_on;value=composition-grammar +stage_field=stage=native-section-symbol;key=required_gates;value=native-section-symbol-green +stage_field=stage=native-section-symbol;key=status;value=complete +stage=id=dna-v2-symbolic-frame +stage_field=stage=dna-v2-symbolic-frame;key=depends_on;value=native-section-symbol +stage_field=stage=dna-v2-symbolic-frame;key=required_gates;value=dna-v2-symbolic-frame-green +stage_field=stage=dna-v2-symbolic-frame;key=status;value=complete +stage=id=projection-symbol-binding +stage_field=stage=projection-symbol-binding;key=depends_on;value=dna-v2-symbolic-frame +stage_field=stage=projection-symbol-binding;key=required_gates;value=projection-symbol-binding-green +stage_field=stage=projection-symbol-binding;key=status;value=complete +stage=id=developer-surface +stage_field=stage=developer-surface;key=depends_on;value=projection-symbol-binding +stage_field=stage=developer-surface;key=required_gates;value=developer-surface-green +stage_field=stage=developer-surface;key=status;value=complete +stage=id=symbolic-commitment-closure +stage_field=stage=symbolic-commitment-closure;key=depends_on;value=developer-surface +stage_field=stage=symbolic-commitment-closure;key=required_gates;value=symbolic-commitment-green +stage_field=stage=symbolic-commitment-closure;key=status;value=complete +history=from_status=current;gates=exact-identity-envelope-green;mode=linear_advance;stage_id=exact-identity-envelope;to_status=complete +history=evidence=local%3Al64s1-self-delimiting-domain-plus-canonical-bytes%2Bexact-roundtrip;kind=dogfood_promotion_receipt;stage_id=exact-identity-envelope +history=from_status=current;gates=seal-coordinate-algebra-green;mode=linear_advance;stage_id=seal-coordinate-algebra;to_status=complete +history=evidence=local%3Asigma-pi-delta-omega%2Bprime-field%2Btyped-axis-law%2Bmillion-sample-audit;kind=dogfood_promotion_receipt;stage_id=seal-coordinate-algebra +history=from_status=current;gates=composition-grammar-green;mode=linear_advance;stage_id=composition-grammar;to_status=complete +history=evidence=local%3Adomain-qualification%2Bordered%2Bcommutative%2Bderivation-composition;kind=dogfood_promotion_receipt;stage_id=composition-grammar +history=from_status=current;gates=native-section-symbol-green;mode=linear_advance;stage_id=native-section-symbol;to_status=complete +history=evidence=local%3Anodes-ports-contexts-routes-localization%2Bexact-state-identity;kind=dogfood_promotion_receipt;stage_id=native-section-symbol +history=from_status=current;gates=dna-v2-symbolic-frame-green;mode=linear_advance;stage_id=dna-v2-symbolic-frame;to_status=complete +history=evidence=local%3A44-byte-frame%2Bv1-explicit-rejection%2Bstale-seal-rejection%2Bexact-fixed-point;kind=dogfood_promotion_receipt;stage_id=dna-v2-symbolic-frame +history=from_status=current;gates=projection-symbol-binding-green;mode=linear_advance;stage_id=projection-symbol-binding;to_status=complete +history=evidence=local%3Aprojection-source-and-replay-bind-native-state-symbol%2Bstale-rejection;kind=dogfood_promotion_receipt;stage_id=projection-symbol-binding +history=from_status=current;gates=developer-surface-green;mode=linear_advance;stage_id=developer-surface;to_status=complete +history=evidence=local%3Aone-line-helpers%2Bparser-safe-ascii%2Breadable-unicode%2Baxis-change-explanations;kind=dogfood_promotion_receipt;stage_id=developer-surface +history=from_status=current;gates=symbolic-commitment-green;mode=linear_advance;stage_id=symbolic-commitment-closure;to_status=complete +history=evidence=github-66e09837ff96e2d10bee30232346e57f9bf7cec9-run-30146463330;kind=dogfood_promotion_receipt;stage_id=symbolic-commitment-closure +END diff --git a/l64-native/Cargo.toml b/l64-native/Cargo.toml index dcd981a..082bc27 100644 --- a/l64-native/Cargo.toml +++ b/l64-native/Cargo.toml @@ -5,4 +5,4 @@ version.workspace = true license.workspace = true [dependencies] -blake3.workspace = true +l64-symbolic = { path = "../l64-symbolic" } diff --git a/l64-native/README.md b/l64-native/README.md index 318096b..53e09d6 100644 --- a/l64-native/README.md +++ b/l64-native/README.md @@ -18,7 +18,7 @@ The second boundary adds a bounded native decoder. Canonical bytes must decode, The third boundary makes primitive execution proof-carrying without adding a receipt schema. Every admitted operation receives a judgment type and kernel witness at routes deterministically composed from the operation route. Generic value insertion cannot construct a witness for a kernel-only judgment. -The fourth boundary adds a native DNA frame with a fixed 44-byte binary header, bounded canonical payload, embedded domain-separated BLAKE3 commitment, and exact DNA decode/re-encode fixed point. The frame contains no string metadata or legacy record payload. +The fourth boundary adds a native DNA frame with a fixed 44-byte binary header, bounded canonical payload, compact state field, and exact DNA decode/re-encode fixed point. The original frame used BLAKE3; the tenth boundary replaces that field with a composed symbolic seal while preserving the header width. The frame contains no string metadata or legacy record payload. The fifth boundary adds a compact authored RNA ingress. `L64R1` uses one declared domain and strictly increasing numeric local slots; routes are composed as `(domain, slot)`. Its byte-oriented instructions lower directly through the existing graph and transaction APIs. Sequencing omits intrinsic evidence nodes because their routes and structure are deterministically derived. @@ -37,8 +37,6 @@ The sixth boundary adds the first native constraint core without creating a para The larger implementation files are factored only at existing item boundaries into construction, typing, transaction, validation, codec, and RNA concerns. This changes review locality without introducing another authority layer or altering canonical bytes. - - The seventh boundary adds proof-producing congruence without promoting a union-find table into authority: - equality is a native type judgment with a deterministically attached equality witness; @@ -53,7 +51,7 @@ The seventh boundary adds proof-producing congruence without promoting a union-f `LOCUS64_PROOF_CONGRUENCE_CHANGE_CHAIN.athens` is complete on repository evidence. The parent execution rail has advanced to incremental dependency closure. -State identity is the domain-separated BLAKE3 commitment of canonical native bytes. No native name, claim identifier, theorem identifier, campaign identifier, JSON field name, or generic serialization schema participates. +Native authority identity is exact: the domain-qualified canonical byte sequence itself. A composed symbolic seal provides a fixed-width state reference and fast inequality check, but seal equality never substitutes for exact canonical comparison. No native name, claim identifier, theorem identifier, campaign identifier, JSON field name, or generic serialization schema participates. The existing `l64-cli` command names now route `L64R1` and `L64D` directly through this native path. Legacy RNA/DNA behavior is classified as compatibility/forensic ingress and is available explicitly through `l64-cli legacy ...`; ambient fallback remains temporarily available with a mandatory deprecation warning. @@ -67,9 +65,9 @@ The eighth boundary adds incremental closure without turning invalidation into a - closure transitions identify the exact reverse-reachable subgraph whose state changed and carry the constraint binding that caused the transition; - independent structure remains outside the affected set; - local and global closure are distinguishable; -- closure queries do not alter canonical bytes, commitments, routes, contexts, or journal history. +- closure queries do not alter canonical bytes, state symbols, routes, contexts, or journal history. -The derived reverse index is an in-memory accelerator only. It is excluded from RNA, DNA, state commitments, and authority identity. +The derived reverse index is an in-memory accelerator only. It is excluded from RNA, DNA, state symbols, and authority identity. The ninth boundary derives the first native upper views without turning any view into authority: @@ -79,8 +77,22 @@ The ninth boundary derives the first native upper views without turning any view - replay is a deterministic view of the native journal and rejects a canonical decode that lacks that runtime history; - reporting counts visible native structure and exposes obligation and invalid routes; - research ranking follows open, invalid, and high-impact reverse-reachable structure; -- every view binds to the native commitment, context, structural counts, journal length, and projection version; +- every view binds to the native state symbol, context, structural counts, journal length, and projection version; - verification rebuilds the complete view and requires exact equality; - the projection crate contains no storage, registry, cache, alternate graph, import, promotion, serialization, or hash authority. `LOCUS64_NATIVE_UPPER_PROJECTION_CHANGE_CHAIN.athens` is complete on repository evidence. The parent execution rail has advanced to legacy authority quarantine. + +The tenth boundary removes BLAKE3 from the native execution and projection spine through a purpose-built symbolic identity system: + +- `l64-symbolic` separates exact identity from compact sealing; +- exact identity is a self-delimiting `L64S1` envelope containing domain and canonical bytes, so authoritative equality is collision-free by representation; +- compact `SymbolicSeal` values expose `Σ`, `Π`, `Δ`, and `Ω` coordinates for aggregate content, ordered composition, adjacent transitions, and nonlinear boundary closure; +- ordered (`⊗`), commutative (`⊕`), derivational (`↦`), and domain-qualified (`∷`) composition are explicit operations rather than hidden byte concatenation; +- `StateSymbol` independently composes nodes, ports, contexts, and routes, allowing a changed state to identify the structural section that moved; +- journal events and projections carry symbolic seals, while `Graph::exact_state_identity()` remains the positive equality boundary; +- DNA v2 retains the 44-byte frame but stores the symbolic state seal; v1 is rejected instead of ambiguously reinterpreted; +- seal equality is only a fast-match signal. Canonical decoding, validation, and exact re-encoding remain mandatory before authority is accepted; +- the native and projection crates contain no BLAKE3 dependency. + +Legacy `l64-core` still uses BLAKE3-backed string digests across its pre-native record, cache, receipt, and packet surfaces. That dependency is now confined to the legacy-authority-quarantine boundary; removing it requires retiring or exactly translating those roles, not substituting another opaque digest. diff --git a/l64-native/src/codec.rs b/l64-native/src/codec.rs index 49418bf..8723564 100644 --- a/l64-native/src/codec.rs +++ b/l64-native/src/codec.rs @@ -3,8 +3,7 @@ use std::collections::BTreeMap; use crate::kernel::{EVIDENCE_LOCUS, EqualityRule, JUDGMENT_LOCUS}; use crate::{ContextDelta, Graph, LocusWord, Node, NodeId, OpCode, Port, PortRole, Route}; -const CODEC_VERSION: u16 = 4; -const COMMITMENT_DOMAIN: &[u8] = b"l64-native-state-v4\0"; +pub(crate) const CODEC_VERSION: u16 = 4; const MAX_NODES: usize = 1 << 20; const MAX_PORTS: usize = 1 << 22; const MAX_CONTEXTS: usize = 1 << 20; diff --git a/l64-native/src/codec/io.rs b/l64-native/src/codec/io.rs index 5820083..5c7e697 100644 --- a/l64-native/src/codec/io.rs +++ b/l64-native/src/codec/io.rs @@ -119,8 +119,7 @@ pub fn decode_canonical(bytes: &[u8]) -> Result { } validate_structure(&nodes, &ports, &contexts, &routes)?; - let commitment = commitment_bytes(bytes); - let graph = Graph::from_decoded_parts(nodes, ports, contexts, routes, commitment); + let graph = Graph::from_decoded_parts(nodes, ports, contexts, routes); graph .validate_decoded_authority() .map_err(|_| DecodeError::InvalidAuthority)?; @@ -129,14 +128,3 @@ pub fn decode_canonical(bytes: &[u8]) -> Result { } Ok(graph) } - -pub(crate) fn state_commitment(graph: &Graph) -> [u8; 32] { - commitment_bytes(&canonical_bytes(graph)) -} - -fn commitment_bytes(bytes: &[u8]) -> [u8; 32] { - let mut hasher = blake3::Hasher::new(); - hasher.update(COMMITMENT_DOMAIN); - hasher.update(bytes); - *hasher.finalize().as_bytes() -} diff --git a/l64-native/src/dna.rs b/l64-native/src/dna.rs index 55717cd..859c2b7 100644 --- a/l64-native/src/dna.rs +++ b/l64-native/src/dna.rs @@ -1,9 +1,10 @@ -use crate::{DecodeError, Graph, canonical_bytes, decode_canonical}; +use crate::{DecodeError, Graph, SymbolicSeal, canonical_bytes, decode_canonical}; +use l64_symbolic::Composer; const DNA_MAGIC: &[u8; 4] = b"L64D"; -const DNA_VERSION: u16 = 1; +const DNA_VERSION: u16 = 2; const DNA_FLAGS: u16 = 0; -const DNA_COMMITMENT_DOMAIN: &[u8] = b"l64-native-dna-v1\0"; +const DNA_SYMBOL_DOMAIN: &str = "l64.native.dna.v2"; const DNA_HEADER_BYTES: usize = 44; pub const MAX_NATIVE_DNA_PAYLOAD_BYTES: usize = 1 << 28; @@ -15,7 +16,7 @@ pub enum DnaError { UnsupportedVersion { version: u16 }, UnsupportedFlags { flags: u16 }, TrailingBytes, - CommitmentMismatch, + SealMismatch, Canonical(DecodeError), } @@ -36,7 +37,7 @@ pub fn dna_bytes(graph: &Graph) -> Result, DnaError> { out.extend_from_slice(&DNA_VERSION.to_le_bytes()); out.extend_from_slice(&DNA_FLAGS.to_le_bytes()); out.extend_from_slice(&(payload.len() as u32).to_le_bytes()); - out.extend_from_slice(&dna_commitment(&payload)); + out.extend_from_slice(&dna_seal(graph, payload.len()).to_bytes()); out.extend_from_slice(&payload); Ok(out) } @@ -73,21 +74,24 @@ pub fn decode_dna(bytes: &[u8]) -> Result { return Err(DnaError::TrailingBytes); } - let stored_commitment: [u8; 32] = bytes[12..44] - .try_into() - .expect("fixed DNA commitment field"); + let stored_seal = SymbolicSeal::from_bytes( + bytes[12..44] + .try_into() + .expect("fixed DNA symbolic seal field"), + ); let payload = &bytes[DNA_HEADER_BYTES..]; - let computed_commitment = dna_commitment(payload); - if stored_commitment != computed_commitment { - return Err(DnaError::CommitmentMismatch); + let graph = decode_canonical(payload)?; + if stored_seal != dna_seal(&graph, payload.len()) { + return Err(DnaError::SealMismatch); } - - Ok(decode_canonical(payload)?) + Ok(graph) } -fn dna_commitment(payload: &[u8]) -> [u8; 32] { - let mut hasher = blake3::Hasher::new(); - hasher.update(DNA_COMMITMENT_DOMAIN); - hasher.update(payload); - *hasher.finalize().as_bytes() +fn dna_seal(graph: &Graph, payload_len: usize) -> SymbolicSeal { + let mut composer = Composer::new(DNA_SYMBOL_DOMAIN); + composer + .u16("version", DNA_VERSION) + .u64("payload-length", payload_len as u64) + .ordered("state", &[graph.state_symbol().root]); + composer.finish() } diff --git a/l64-native/src/graph.rs b/l64-native/src/graph.rs index 1039764..b06bb12 100644 --- a/l64-native/src/graph.rs +++ b/l64-native/src/graph.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use crate::kernel::{ConstraintState, EqualityRule, EvidencePlan}; use crate::{ ClosureState, ClosureTransition, ConstraintKind, ContextDelta, Dimension, JournalEvent, - LocusWord, Obstruction, OpCode, Port, PortRole, Route, + LocusWord, Obstruction, OpCode, Port, PortRole, Route, StateSymbol, SymbolicSeal, }; pub type NodeId = u32; @@ -80,7 +80,7 @@ pub struct Graph { contexts: Vec, routes: BTreeMap, journal: Vec, - commitment: [u8; 32], + symbol: StateSymbol, derived: DerivedIndex, } diff --git a/l64-native/src/graph/construction.rs b/l64-native/src/graph/construction.rs index 6291a9f..673189f 100644 --- a/l64-native/src/graph/construction.rs +++ b/l64-native/src/graph/construction.rs @@ -4,7 +4,6 @@ impl Graph { ports: Vec, contexts: Vec, routes: BTreeMap, - commitment: [u8; 32], ) -> Self { let mut graph = Self { nodes, @@ -12,10 +11,11 @@ impl Graph { contexts, routes, journal: Vec::new(), - commitment, + symbol: StateSymbol::ZERO, derived: DerivedIndex::default(), }; graph.rebuild_derived_index(); + graph.symbol = crate::symbol::state_symbol(&graph); graph } @@ -26,10 +26,10 @@ impl Graph { contexts: vec![ContextDelta::root()], routes: BTreeMap::new(), journal: Vec::new(), - commitment: [0; 32], + symbol: StateSymbol::ZERO, derived: DerivedIndex::empty(1), }; - graph.commitment = crate::codec::state_commitment(&graph); + graph.symbol = crate::symbol::state_symbol(&graph); graph } @@ -66,8 +66,12 @@ impl Graph { self.journal.len() } - pub fn state_commitment(&self) -> [u8; 32] { - self.commitment + pub fn state_symbol(&self) -> StateSymbol { + self.symbol + } + + pub fn exact_state_identity(&self) -> crate::SymbolicIdentity { + crate::state_identity(self) } pub fn journal(&self) -> &[JournalEvent] { @@ -82,13 +86,13 @@ impl Graph { self.ensure_context(parent)?; self.ensure_node(binding)?; self.validate_context_binding(parent, binding)?; - let before = self.commitment; + let before = self.symbol.root; let id = self.contexts.len() as ContextId; self.contexts.push(ContextDelta { parent, binding }); self.derived.by_context.push(Vec::new()); - let after = crate::codec::state_commitment(self); + let after = crate::symbol::state_symbol(self).root; self.push_event(OpCode::ExtendContext, binding, before, after); - self.commitment = after; + self.symbol = crate::symbol::state_symbol(self); Ok(id) } @@ -212,7 +216,7 @@ impl Graph { evidence_plan: EvidencePlan, ) -> crate::CommitResult { let [route, judgment_route, evidence_route] = routes; - let before = self.commitment; + let before = self.symbol.root; let operation = self.nodes.len() as NodeId; let operation_first_port = self.ports.len() as u32; @@ -276,14 +280,14 @@ impl Graph { self.register_derived_node(judgment); self.register_derived_node(evidence); - let after = crate::codec::state_commitment(self); + let after = crate::symbol::state_symbol(self).root; self.push_event(opcode, operation, before, after); - self.commitment = after; + self.symbol = crate::symbol::state_symbol(self); crate::CommitResult { node: operation, evidence, event: self.journal.len().saturating_sub(1) as EventId, - commitment: after, + symbol: after, } } @@ -297,7 +301,7 @@ impl Graph { premises: &[NodeId], ) -> crate::CommitResult { let [route, evidence_route] = routes; - let before = self.commitment; + let before = self.symbol.root; let judgment = self.nodes.len() as NodeId; let judgment_first_port = self.ports.len() as u32; @@ -333,14 +337,14 @@ impl Graph { self.routes.insert(evidence_route, evidence); self.register_derived_node(judgment); self.register_derived_node(evidence); - let after = crate::codec::state_commitment(self); + let after = crate::symbol::state_symbol(self).root; self.push_event(OpCode::EqualityWitness, judgment, before, after); - self.commitment = after; + self.symbol = crate::symbol::state_symbol(self); crate::CommitResult { node: judgment, evidence, event: self.journal.len().saturating_sub(1) as EventId, - commitment: after, + symbol: after, } } } diff --git a/l64-native/src/graph/storage.rs b/l64-native/src/graph/storage.rs index 0383f9a..6838311 100644 --- a/l64-native/src/graph/storage.rs +++ b/l64-native/src/graph/storage.rs @@ -68,7 +68,7 @@ impl Graph { self.ensure_type(ty)?; } - let before = self.commitment; + let before = self.symbol.root; let node = self.nodes.len() as NodeId; let first_port = self.ports.len() as u32; self.ports.extend_from_slice(ports); @@ -82,9 +82,9 @@ impl Graph { }); self.routes.insert(route, node); self.register_derived_node(node); - let after = crate::codec::state_commitment(self); + let after = crate::symbol::state_symbol(self).root; self.push_event(opcode, node, before, after); - self.commitment = after; + self.symbol = crate::symbol::state_symbol(self); Ok(node) } @@ -92,8 +92,8 @@ impl Graph { &mut self, operation: OpCode, subject: NodeId, - before: [u8; 32], - after: [u8; 32], + before: SymbolicSeal, + after: SymbolicSeal, ) { let parent = self .journal diff --git a/l64-native/src/journal.rs b/l64-native/src/journal.rs index 61a71d6..9c10537 100644 --- a/l64-native/src/journal.rs +++ b/l64-native/src/journal.rs @@ -1,11 +1,11 @@ -use crate::{EventId, NodeId, OpCode}; +use crate::{EventId, NodeId, OpCode, SymbolicSeal}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct JournalEvent { pub(crate) operation: OpCode, pub(crate) subject: NodeId, - pub(crate) before: [u8; 32], - pub(crate) after: [u8; 32], + pub(crate) before: SymbolicSeal, + pub(crate) after: SymbolicSeal, pub(crate) parent: EventId, } @@ -18,11 +18,11 @@ impl JournalEvent { self.subject } - pub fn before(&self) -> [u8; 32] { + pub fn before(&self) -> SymbolicSeal { self.before } - pub fn after(&self) -> [u8; 32] { + pub fn after(&self) -> SymbolicSeal { self.after } diff --git a/l64-native/src/kernel/proposal.rs b/l64-native/src/kernel/proposal.rs index 6e584ab..71b1846 100644 --- a/l64-native/src/kernel/proposal.rs +++ b/l64-native/src/kernel/proposal.rs @@ -106,7 +106,7 @@ pub struct CommitResult { pub node: NodeId, pub evidence: NodeId, pub event: u32, - pub commitment: [u8; 32], + pub symbol: crate::SymbolicSeal, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -225,4 +225,3 @@ pub enum Obstruction { binding: NodeId, }, } - diff --git a/l64-native/src/lib.rs b/l64-native/src/lib.rs index 0cfefb3..e8da9f4 100644 --- a/l64-native/src/lib.rs +++ b/l64-native/src/lib.rs @@ -10,6 +10,7 @@ mod journal; mod kernel; mod rna; mod route; +mod symbol; pub use closure::{ClosureState, ClosureTransition}; pub use codec::{DecodeError, canonical_bytes, decode_canonical}; @@ -19,7 +20,9 @@ pub use dna::{DnaError, MAX_NATIVE_DNA_PAYLOAD_BYTES, decode_dna, dna_bytes}; pub use graph::{ContextId, EventId, Graph, Node, NodeId, ROOT_CONTEXT}; pub use journal::JournalEvent; pub use kernel::{CommitResult, ConstraintKind, Obstruction, OpCode, Port, PortRole, Proposal}; +pub use l64_symbolic::{SymbolicIdentity, SymbolicSeal}; pub use rna::{ MAX_NATIVE_RNA_BYTES, RnaError, compile_rna, dna_to_rna, normalize_rna, rna_bytes, rna_to_dna, }; pub use route::{LocusWord, Route}; +pub use symbol::{STATE_SYMBOL_DOMAIN, StateSymbol, state_identity}; diff --git a/l64-native/src/symbol.rs b/l64-native/src/symbol.rs new file mode 100644 index 0000000..c6b04e5 --- /dev/null +++ b/l64-native/src/symbol.rs @@ -0,0 +1,134 @@ +use core::fmt; + +use crate::{ContextDelta, Graph, Node, NodeId, Port, Route, canonical_bytes}; +use l64_symbolic::{Composer, SymbolicIdentity, SymbolicSeal}; + +pub const STATE_SYMBOL_DOMAIN: &str = "l64.native.state.v2"; +const NODE_DOMAIN: &str = "l64.native.node.v1"; +const PORT_DOMAIN: &str = "l64.native.port.v1"; +const CONTEXT_DOMAIN: &str = "l64.native.context.v1"; +const ROUTE_DOMAIN: &str = "l64.native.route.v1"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct StateSymbol { + pub root: SymbolicSeal, + pub nodes: SymbolicSeal, + pub ports: SymbolicSeal, + pub contexts: SymbolicSeal, + pub routes: SymbolicSeal, +} + +impl StateSymbol { + pub const ZERO: Self = Self { + root: SymbolicSeal::ZERO, + nodes: SymbolicSeal::ZERO, + ports: SymbolicSeal::ZERO, + contexts: SymbolicSeal::ZERO, + routes: SymbolicSeal::ZERO, + }; + + pub fn changed_sections(self, other: Self) -> [bool; 4] { + [ + self.nodes != other.nodes, + self.ports != other.ports, + self.contexts != other.contexts, + self.routes != other.routes, + ] + } +} + +impl fmt::Display for StateSymbol { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "{} ∣ nodes={} ports={} contexts={} routes={}", + self.root.pretty(STATE_SYMBOL_DOMAIN), + self.nodes, + self.ports, + self.contexts, + self.routes + ) + } +} + +pub fn state_identity(graph: &Graph) -> SymbolicIdentity { + SymbolicIdentity::new(STATE_SYMBOL_DOMAIN, canonical_bytes(graph)) +} + +pub(crate) fn state_symbol(graph: &Graph) -> StateSymbol { + let nodes = ordered_section("nodes", graph.nodes_raw().iter().map(node_seal)); + let ports = ordered_section("ports", graph.ports_raw().iter().map(port_seal)); + let contexts = ordered_section("contexts", graph.contexts_raw().iter().map(context_seal)); + let routes = ordered_section( + "routes", + graph + .routes_raw() + .iter() + .map(|(route, node)| route_seal(route, *node)), + ); + let mut root = Composer::new(STATE_SYMBOL_DOMAIN); + root.u16("codec", crate::codec::CODEC_VERSION) + .u64("node-count", graph.nodes_raw().len() as u64) + .u64("port-count", graph.ports_raw().len() as u64) + .u64("context-count", graph.contexts_raw().len() as u64) + .u64("route-count", graph.routes_raw().len() as u64) + .ordered("sections", &[nodes, ports, contexts, routes]); + StateSymbol { + root: root.finish(), + nodes, + ports, + contexts, + routes, + } +} + +fn ordered_section(label: &str, parts: impl Iterator) -> SymbolicSeal { + let parts = parts.collect::>(); + let mut composer = Composer::new("l64.native.section.v1"); + composer.ordered(label, &parts); + composer.finish() +} + +fn node_seal(node: &Node) -> SymbolicSeal { + let range = node.port_range(); + let mut composer = Composer::new(NODE_DOMAIN); + composer + .u16("opcode", node.opcode() as u16) + .u32("context", node.context()) + .u32("type", node.ty().unwrap_or(NodeId::MAX)) + .u64("payload", node.payload()) + .u32("first-port", range.start as u32) + .u16("port-count", (range.end - range.start) as u16); + composer.finish() +} + +fn port_seal(port: &Port) -> SymbolicSeal { + let mut composer = Composer::new(PORT_DOMAIN); + composer + .u32("target", port.target()) + .u8("role", port.role() as u8) + .u8("flags", port.flags()) + .u16("ordinal", port.ordinal()); + composer.finish() +} + +fn context_seal(context: &ContextDelta) -> SymbolicSeal { + let mut composer = Composer::new(CONTEXT_DOMAIN); + composer + .u32("parent", context.parent()) + .u32("binding", context.binding().unwrap_or(NodeId::MAX)); + composer.finish() +} + +fn route_seal(route: &Route, node: NodeId) -> SymbolicSeal { + let mut tail = Vec::with_capacity(route.tail().len() * 8); + for word in route.tail() { + tail.extend_from_slice(&word.0.to_le_bytes()); + } + let mut composer = Composer::new(ROUTE_DOMAIN); + composer + .u64("domain", route.domain().0) + .field("tail", &tail) + .u32("node", node); + composer.finish() +} diff --git a/l64-native/tests/architecture.rs b/l64-native/tests/architecture.rs index 860c283..daa3b6a 100644 --- a/l64-native/tests/architecture.rs +++ b/l64-native/tests/architecture.rs @@ -49,6 +49,7 @@ fn native_source_rejects_coordination_heavy_dependencies() { ["HashMap", "<", "Str", "ing"].concat(), ["serde", "_json"].concat(), ["bin", "code"].concat(), + ["blake", "3"].concat(), ["Ser", "ialize"].concat(), ["Deser", "ialize"].concat(), ["claim", "_packet"].concat(), diff --git a/l64-native/tests/closure.rs b/l64-native/tests/closure.rs index cb2fe05..7168fdd 100644 --- a/l64-native/tests/closure.rs +++ b/l64-native/tests/closure.rs @@ -312,7 +312,7 @@ fn closure_queries_do_not_mutate_authority_or_canonical_bytes() { .unwrap(); let child = fixture.graph.extend_context(ROOT_CONTEXT, cause).unwrap(); let before = canonical_bytes(&fixture.graph); - let commitment = fixture.graph.state_commitment(); + let commitment = fixture.graph.state_symbol(); let journal = fixture.graph.journal_len(); let _ = fixture.graph.direct_dependents(fixture.subject).unwrap(); @@ -325,6 +325,6 @@ fn closure_queries_do_not_mutate_authority_or_canonical_bytes() { let _ = fixture.graph.global_closure().unwrap(); assert_eq!(canonical_bytes(&fixture.graph), before); - assert_eq!(fixture.graph.state_commitment(), commitment); + assert_eq!(fixture.graph.state_symbol(), commitment); assert_eq!(fixture.graph.journal_len(), journal); } diff --git a/l64-native/tests/codec.rs b/l64-native/tests/codec.rs index 32336e9..7bceb9c 100644 --- a/l64-native/tests/codec.rs +++ b/l64-native/tests/codec.rs @@ -39,7 +39,7 @@ fn canonical_decode_reencode_is_exact_fixed_point() { let decoded = decode_canonical(&bytes).unwrap(); assert_eq!(canonical_bytes(&decoded), bytes); - assert_eq!(decoded.state_commitment(), graph.state_commitment()); + assert_eq!(decoded.state_symbol(), graph.state_symbol()); assert_eq!(decoded.node_count(), graph.node_count()); assert_eq!(decoded.port_count(), graph.port_count()); assert_eq!(decoded.context_count(), graph.context_count()); diff --git a/l64-native/tests/dna.rs b/l64-native/tests/dna.rs index 7e4c45a..91944f3 100644 --- a/l64-native/tests/dna.rs +++ b/l64-native/tests/dna.rs @@ -29,7 +29,7 @@ fn native_dna_roundtrip_is_exact_fixed_point() { let decoded = decode_dna(&dna).unwrap(); assert_eq!(dna_bytes(&decoded).unwrap(), dna); - assert_eq!(decoded.state_commitment(), graph.state_commitment()); + assert_eq!(decoded.state_symbol(), graph.state_symbol()); assert_eq!(decoded.journal_len(), 0); assert!(!dna.windows(b"claim".len()).any(|window| window == b"claim")); assert!( @@ -39,23 +39,40 @@ fn native_dna_roundtrip_is_exact_fixed_point() { } #[test] -fn native_dna_rejects_payload_tampering() { +fn native_dna_rejects_header_seal_tampering() { let mut dna = dna_bytes(&proven_graph()).unwrap(); - let last = dna.len() - 1; - dna[last] ^= 1; - assert_eq!(decode_dna(&dna).unwrap_err(), DnaError::CommitmentMismatch); + dna[12] ^= 1; + assert_eq!(decode_dna(&dna).unwrap_err(), DnaError::SealMismatch); +} + +#[test] +fn native_dna_rejects_valid_alternate_payload_under_stale_seal() { + let mut left = Graph::new(); + left.declare_atom_type(route(1), LocusWord(0x41)).unwrap(); + let mut right = Graph::new(); + right.declare_atom_type(route(1), LocusWord(0x42)).unwrap(); + + let mut dna = dna_bytes(&left).unwrap(); + let right_dna = dna_bytes(&right).unwrap(); + assert_eq!(dna.len(), right_dna.len()); + dna[44..].copy_from_slice(&right_dna[44..]); + assert_eq!(decode_dna(&dna).unwrap_err(), DnaError::SealMismatch); } #[test] fn native_dna_rejects_unknown_header_state() { let original = dna_bytes(&proven_graph()).unwrap(); - let mut version = original.clone(); - version[4..6].copy_from_slice(&2u16.to_le_bytes()); - assert_eq!( - decode_dna(&version).unwrap_err(), - DnaError::UnsupportedVersion { version: 2 } - ); + for unsupported in [1_u16, 3_u16] { + let mut version = original.clone(); + version[4..6].copy_from_slice(&unsupported.to_le_bytes()); + assert_eq!( + decode_dna(&version).unwrap_err(), + DnaError::UnsupportedVersion { + version: unsupported, + } + ); + } let mut flags = original; flags[6..8].copy_from_slice(&1u16.to_le_bytes()); diff --git a/l64-native/tests/equality.rs b/l64-native/tests/equality.rs index 886ff7a..58f9f77 100644 --- a/l64-native/tests/equality.rs +++ b/l64-native/tests/equality.rs @@ -203,7 +203,7 @@ fn invalid_merges_are_rejected_before_state_mutation() { graph.node_count(), graph.port_count(), graph.journal_len(), - graph.state_commitment(), + graph.state_symbol(), ); assert!(matches!( @@ -215,7 +215,7 @@ fn invalid_merges_are_rejected_before_state_mutation() { graph.node_count(), graph.port_count(), graph.journal_len(), - graph.state_commitment(), + graph.state_symbol(), ), before ); diff --git a/l64-native/tests/symbol.rs b/l64-native/tests/symbol.rs new file mode 100644 index 0000000..39480d5 --- /dev/null +++ b/l64-native/tests/symbol.rs @@ -0,0 +1,65 @@ +use l64_native::{Graph, LocusWord, ROOT_CONTEXT, Route, canonical_bytes, decode_canonical}; + +fn route(index: u64) -> Route { + Route::root(LocusWord(0x53594d424f4c4943)).composed(LocusWord(index)) +} + +#[test] +fn exact_state_identity_is_canonical_byte_identity() { + let mut left = Graph::new(); + left.declare_atom_type(route(1), LocusWord(0x41)).unwrap(); + + let mut right = Graph::new(); + right.declare_atom_type(route(1), LocusWord(0x42)).unwrap(); + + let left_identity = left.exact_state_identity(); + let right_identity = right.exact_state_identity(); + assert!(left_identity.verify_exact("l64.native.state.v2", &canonical_bytes(&left))); + assert_ne!(left_identity, right_identity); + assert_ne!(left_identity.seal(), right_identity.seal()); +} + +#[test] +fn state_symbol_localizes_structural_change_classes() { + let mut graph = Graph::new(); + let empty = graph.state_symbol(); + + let atom = graph.declare_atom_type(route(1), LocusWord(0x41)).unwrap(); + let after_atom = graph.state_symbol(); + assert_eq!( + empty.changed_sections(after_atom), + [true, false, false, true] + ); + + graph.declare_function_type(route(2), atom, atom).unwrap(); + let after_function = graph.state_symbol(); + assert_eq!( + after_atom.changed_sections(after_function), + [true, true, false, true] + ); + + graph.extend_context(ROOT_CONTEXT, atom).unwrap(); + let after_context = graph.state_symbol(); + assert_eq!( + after_function.changed_sections(after_context), + [false, false, true, false] + ); +} + +#[test] +fn symbolic_and_exact_identity_survive_canonical_fixed_point() { + let mut graph = Graph::new(); + let atom = graph.declare_atom_type(route(1), LocusWord(0x41)).unwrap(); + graph.declare_function_type(route(2), atom, atom).unwrap(); + graph.extend_context(ROOT_CONTEXT, atom).unwrap(); + + let bytes = canonical_bytes(&graph); + let decoded = decode_canonical(&bytes).unwrap(); + assert_eq!(decoded.state_symbol(), graph.state_symbol()); + assert_eq!(decoded.exact_state_identity(), graph.exact_state_identity()); + assert!( + decoded + .exact_state_identity() + .verify_exact("l64.native.state.v2", &bytes) + ); +} diff --git a/l64-native/tests/workloads.rs b/l64-native/tests/workloads.rs index 77ff234..0510a5f 100644 --- a/l64-native/tests/workloads.rs +++ b/l64-native/tests/workloads.rs @@ -15,7 +15,7 @@ fn valid_typed_function_composition_commits() { let ac = graph.declare_function_type(route(6), a, c).unwrap(); let first = graph.insert_value(route(7), ROOT_CONTEXT, ab).unwrap(); let second = graph.insert_value(route(8), ROOT_CONTEXT, bc).unwrap(); - let before = graph.state_commitment(); + let before = graph.state_symbol(); let before_nodes = graph.node_count(); let committed = graph @@ -23,7 +23,7 @@ fn valid_typed_function_composition_commits() { .unwrap(); assert_eq!(graph.node_count(), before_nodes + 3); - assert_ne!(graph.state_commitment(), before); + assert_ne!(graph.state_symbol(), before); assert_eq!(graph.node(committed.node).unwrap().ty(), Some(ac)); assert_eq!(graph.ports(committed.node).unwrap().len(), 2); let evidence = graph.node(committed.evidence).unwrap(); @@ -31,10 +31,7 @@ fn valid_typed_function_composition_commits() { let judgment = evidence.ty().unwrap(); assert_eq!(graph.node(judgment).unwrap().opcode(), OpCode::TypeJudgment); assert_eq!(graph.ports(judgment).unwrap().len(), 4); - assert_eq!( - graph.journal().last().unwrap().after(), - committed.commitment - ); + assert_eq!(graph.journal().last().unwrap().after(), committed.symbol); } #[test] @@ -51,7 +48,7 @@ fn invalid_matrix_shape_rejects_without_mutation() { .insert_value(route(16), ROOT_CONTEXT, right_ty) .unwrap(); - let before_commitment = graph.state_commitment(); + let before_commitment = graph.state_symbol(); let before_nodes = graph.node_count(); let before_ports = graph.port_count(); let before_contexts = graph.context_count(); @@ -72,7 +69,7 @@ fn invalid_matrix_shape_rejects_without_mutation() { right_rows: 4, }) ); - assert_eq!(graph.state_commitment(), before_commitment); + assert_eq!(graph.state_symbol(), before_commitment); assert_eq!(graph.node_count(), before_nodes); assert_eq!(graph.port_count(), before_ports); assert_eq!(graph.context_count(), before_contexts); @@ -101,14 +98,14 @@ fn kernel_judgment_cannot_be_forged_through_value_insertion() { )) .unwrap(); let judgment = graph.node(committed.evidence).unwrap().ty().unwrap(); - let before = graph.state_commitment(); + let before = graph.state_symbol(); let before_nodes = graph.node_count(); assert_eq!( graph.insert_value(route(40), ROOT_CONTEXT, judgment), Err(Obstruction::EvidenceOnlyType { node: judgment }) ); - assert_eq!(graph.state_commitment(), before); + assert_eq!(graph.state_symbol(), before); assert_eq!(graph.node_count(), before_nodes); assert!(graph.resolve(&route(40)).is_none()); } diff --git a/l64-projection/README.md b/l64-projection/README.md index e876655..9df38b6 100644 --- a/l64-projection/README.md +++ b/l64-projection/README.md @@ -10,6 +10,6 @@ It provides five coordinated views over one native state and context: - deterministic aggregate reporting; - ranked research candidates from open and invalid reverse-reachable structure. -Every projection carries its native state commitment, context, structural counts, journal length, and projection version. Verification rebuilds the complete view from the graph and requires exact equality. The crate has no storage, registry, cache, import, promotion, serialization, hashing, or alternate graph dependency. +Every projection carries its native composed state symbol, context, structural counts, journal length, and projection version. Verification rebuilds the complete view from the graph and requires exact equality. The crate has no storage, registry, cache, import, promotion, serialization, independent hashing, or alternate graph dependency. It reuses the symbolic identity carried by native authority. Projection records are expendable. Native authority remains canonical. diff --git a/l64-projection/src/replay.rs b/l64-projection/src/replay.rs index a0bfc20..bb7a8d5 100644 --- a/l64-projection/src/replay.rs +++ b/l64-projection/src/replay.rs @@ -1,5 +1,5 @@ use crate::{ProjectionError, ProjectionSource, SourceRef, source_ref}; -use l64_native::{ContextId, Graph, OpCode}; +use l64_native::{ContextId, Graph, OpCode, SymbolicSeal}; use std::collections::BTreeSet; #[derive(Debug, Clone, PartialEq, Eq)] @@ -7,8 +7,8 @@ pub struct ReplayStep { pub event: u32, pub operation: OpCode, pub subject: SourceRef, - pub before: [u8; 32], - pub after: [u8; 32], + pub before: SymbolicSeal, + pub after: SymbolicSeal, pub parent: Option, } diff --git a/l64-projection/src/set.rs b/l64-projection/src/set.rs index 39765c7..1b38c08 100644 --- a/l64-projection/src/set.rs +++ b/l64-projection/src/set.rs @@ -46,7 +46,7 @@ impl ProjectionSet { let mut out = String::new(); let _ = writeln!(out, "L64 NATIVE PROJECTION v{}", self.source.version); let _ = writeln!(out, "context={}", self.source.context); - let _ = writeln!(out, "commitment={}", hex(&self.source.commitment)); + let _ = writeln!(out, "symbol={}", self.source.symbol); let _ = writeln!(out, "nodes={}", self.source.node_count); let _ = writeln!(out, "contexts={}", self.source.context_count); let _ = writeln!(out, "journal={}", self.source.journal_len); @@ -88,14 +88,6 @@ fn format_route(route: &Route) -> String { out } -fn hex(bytes: &[u8]) -> String { - let mut out = String::with_capacity(bytes.len() * 2); - for byte in bytes { - let _ = write!(out, "{byte:02x}"); - } - out -} - #[cfg(test)] mod tests { use super::*; diff --git a/l64-projection/src/source.rs b/l64-projection/src/source.rs index ea32704..aca7770 100644 --- a/l64-projection/src/source.rs +++ b/l64-projection/src/source.rs @@ -1,5 +1,7 @@ use crate::PROJECTION_VERSION; -use l64_native::{ClosureState, ContextId, Graph, NodeId, Obstruction, OpCode, PortRole, Route}; +use l64_native::{ + ClosureState, ContextId, Graph, NodeId, Obstruction, OpCode, PortRole, Route, StateSymbol, +}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum ProjectionError { @@ -20,7 +22,7 @@ impl From for ProjectionError { #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProjectionSource { - pub commitment: [u8; 32], + pub symbol: StateSymbol, pub context: ContextId, pub node_count: usize, pub context_count: usize, @@ -34,7 +36,7 @@ impl ProjectionSource { .context(context) .ok_or(ProjectionError::UnknownContext(context))?; Ok(Self { - commitment: graph.state_commitment(), + symbol: graph.state_symbol(), context, node_count: graph.node_count(), context_count: graph.context_count(), @@ -53,7 +55,7 @@ impl ProjectionSource { graph .context(self.context) .ok_or(ProjectionError::UnknownContext(self.context))?; - if self.commitment != graph.state_commitment() + if self.symbol != graph.state_symbol() || self.node_count != graph.node_count() || self.context_count != graph.context_count() || self.journal_len != graph.journal_len() diff --git a/l64-projection/tests/projection.rs b/l64-projection/tests/projection.rs index 72ace28..8233cdc 100644 --- a/l64-projection/tests/projection.rs +++ b/l64-projection/tests/projection.rs @@ -73,7 +73,7 @@ fn fixture() -> Fixture { fn all_upper_views_are_derived_without_mutating_native_authority() { let fixture = fixture(); let before = canonical_bytes(&fixture.graph); - let commitment = fixture.graph.state_commitment(); + let commitment = fixture.graph.state_symbol(); let counts = ( fixture.graph.node_count(), fixture.graph.port_count(), @@ -85,7 +85,7 @@ fn all_upper_views_are_derived_without_mutating_native_authority() { projection.verify(&fixture.graph).unwrap(); assert_eq!(canonical_bytes(&fixture.graph), before); - assert_eq!(fixture.graph.state_commitment(), commitment); + assert_eq!(fixture.graph.state_symbol(), commitment); assert_eq!( ( fixture.graph.node_count(), @@ -250,7 +250,7 @@ fn replay_projection_is_bound_to_the_actual_runtime_journal() { let fixture = fixture(); let projection = ProjectionSet::derive(&fixture.graph, ROOT_CONTEXT, 8).unwrap(); let decoded = decode_canonical(&canonical_bytes(&fixture.graph)).unwrap(); - assert_eq!(decoded.state_commitment(), fixture.graph.state_commitment()); + assert_eq!(decoded.state_symbol(), fixture.graph.state_symbol()); assert_eq!(decoded.journal_len(), 0); assert_eq!( projection.verify(&decoded), diff --git a/l64-symbolic/Cargo.toml b/l64-symbolic/Cargo.toml new file mode 100644 index 0000000..edcb7c5 --- /dev/null +++ b/l64-symbolic/Cargo.toml @@ -0,0 +1,5 @@ +[package] +name = "l64-symbolic" +edition.workspace = true +version.workspace = true +license.workspace = true diff --git a/l64-symbolic/DESIGN.md b/l64-symbolic/DESIGN.md new file mode 100644 index 0000000..036b436 --- /dev/null +++ b/l64-symbolic/DESIGN.md @@ -0,0 +1,75 @@ +# Symbolic commitment law + +## 1. Separation of authority and acceleration + +For domain `D` and canonical structure `x`, exact identity is the self-delimiting term + +```text +I(D, x) = L64S1 ‖ |D| ‖ |x| ‖ D ‖ x +``` + +where lengths are fixed-width little-endian integers. Equality of `I(D, x)` is ordinary byte equality, so no collision assumption is involved. + +The compact seal + +```text +S(D, x) = ⟦D ∷ Σ(x) ⊗ Π(x) ⊗ Δ(x) ⊗ Ω(x)⟧ +``` + +is a 256-bit projection of the exact term. `S(a) ≠ S(b)` proves inequality. `S(a) = S(b)` is only permission to continue to exact comparison. + +## 2. Composition grammar + +- `∷` qualifies a term by domain. +- `⊗` composes an ordered sequence; permutation changes the result. +- `⊕` composes a canonical set after sorting; permutation does not change the result. +- `↦` binds a source, relation, and result as a derivation. +- `⟦…⟧` delimits a symbolic commitment expression. + +Every field is length-delimited and marked by a distinct boundary token. Domain, label, value, order, segment count, and total absorbed length therefore occupy explicit positions in the input algebra. + +## 3. Coordinate roles + +All arithmetic is performed modulo the prime `2^64 - 59`. + +### `Σ` — aggregate content mass + +A weighted modular sum over tokens and position. It intentionally changes more locally than the other coordinates and supports coarse change diagnosis. + +### `Π` — ordered composition + +A Horner-style polynomial recurrence. It distinguishes sequence order and broadly diffuses changes. + +### `Δ` — adjacent transition structure + +A recurrence over squared token-to-token differences. It distinguishes changes in local transitions even when aggregate content is similar. + +### `Ω` — nonlinear boundary closure + +A quadratic recurrence influenced by token, position, and segment boundaries. It supplies broad diffusion and boundary sensitivity. + +The coordinates are deliberately nonredundant. A seal is interpreted as their conjunction, not as four interchangeable random words. + +## 4. Native state decomposition + +The native graph derives independent seals for: + +```text +nodes ⊗ ports ⊗ contexts ⊗ routes +``` + +The root state symbol composes these section seals with codec version and section counts. This permits fast rejection and localizes which structural family changed without introducing a second graph or stored index. + +## 5. Verification law + +1. Reject malformed framing or a mismatched compact seal. +2. Decode the canonical payload under native structural law. +3. Re-execute validation rules. +4. Re-encode canonically and require the exact fixed point. +5. Use exact identity for positive authority equality. + +No compact symbolic seal, including a matching seal, may bypass steps 2–5. + +## 6. Security and trust boundary + +The seal is not claimed as a cryptographic hash, signature, MAC, or adversarial authenticity mechanism. An unkeyed digest does not establish who produced an artifact. Authenticity belongs to signed transport or another explicit trust carrier. The symbolic system replaces BLAKE3 inside native state identity by removing probabilistic equality from the authority decision, not by making an unsupported cryptographic claim. diff --git a/l64-symbolic/README.md b/l64-symbolic/README.md new file mode 100644 index 0000000..feffbdb --- /dev/null +++ b/l64-symbolic/README.md @@ -0,0 +1,33 @@ +# l64-symbolic + +`l64-symbolic` separates **exact symbolic identity** from **compact symbolic sealing**. + +- Exact identity is the domain plus canonical bytes. Equality is therefore injective by representation, not probabilistic. +- A `SymbolicSeal` is a fixed-width, composable projection used for fast inequality, frame corruption checks, indexing, and developer-facing state references. It is never sufficient authority for equality. + +## Algebra + +A composer absorbs an explicitly tagged token stream under the prime modulus `2^64 - 59` and exposes four independently purposed coordinates: + +- `Σ` — weighted aggregate mass; +- `Π` — ordered Horner composition; +- `Δ` — adjacency/change structure; +- `Ω` — nonlinear boundary closure. + +Ordered composition uses `⊗`; commutative composition uses `⊕` after canonical sorting; derivation uses `↦`; domain qualification uses `∷`; `⟦…⟧` marks the symbolic boundary. + +The pretty form is intentionally readable: + +```text +⟦l64.native.state.v2 ∷ Σ… ⊗ Π… ⊗ Δ… ⊗ Ω…⟧ +``` + +The stable ASCII form is parser-safe: + +```text +s1:... +``` + +## Security boundary + +The compact seal is not presented as a new cryptographic primitive. Exact canonical bytes remain authority. A seal may reject inequality early, but positive equality requires exact canonical reconstruction or byte equality. This gives Locus64 stronger system-level discipline than substituting an unaudited home-grown digest for BLAKE3. diff --git a/l64-symbolic/examples/symbolic_audit.rs b/l64-symbolic/examples/symbolic_audit.rs new file mode 100644 index 0000000..1fdbd77 --- /dev/null +++ b/l64-symbolic/examples/symbolic_audit.rs @@ -0,0 +1,48 @@ +use l64_symbolic::{Composer, SealAxis, SymbolicSeal}; +use std::collections::BTreeSet; + +fn seal(value: u64) -> SymbolicSeal { + let mut composer = Composer::new("l64.audit.u64"); + composer.u64("value", value); + composer.finish() +} + +fn bit_distance(left: u64, right: u64) -> u32 { + (left ^ right).count_ones() +} + +fn main() { + const SAMPLES: u64 = 1_000_000; + let mut full = BTreeSet::new(); + let mut axes = [ + BTreeSet::new(), + BTreeSet::new(), + BTreeSet::new(), + BTreeSet::new(), + ]; + let mut diffusion = [0_u64; 4]; + + for value in 0..SAMPLES { + let current = seal(value); + assert!(full.insert(current), "full seal collision at {value}"); + for (index, coordinate) in current.axes().into_iter().enumerate() { + axes[index].insert(coordinate); + } + let flipped = seal(value ^ (1_u64 << (value % 64))); + for (index, total) in diffusion.iter_mut().enumerate() { + *total += bit_distance(current.axes()[index], flipped.axes()[index]) as u64; + } + } + + println!("samples={SAMPLES}"); + println!("full_unique={}", full.len()); + for (index, axis) in SealAxis::ALL.into_iter().enumerate() { + println!( + "{} role={} unique={} mean_bit_change={:.3}", + axis.symbol(), + axis.role(), + axes[index].len(), + diffusion[index] as f64 / SAMPLES as f64 + ); + } +} diff --git a/l64-symbolic/examples/symbolic_commitment.rs b/l64-symbolic/examples/symbolic_commitment.rs new file mode 100644 index 0000000..c05d41a --- /dev/null +++ b/l64-symbolic/examples/symbolic_commitment.rs @@ -0,0 +1,28 @@ +use l64_symbolic::{Composer, exact_identity}; + +fn main() { + let source = exact_identity("l64.example.source", b"typed(a -> b)"); + let result = exact_identity("l64.example.result", b"typed(a -> c)"); + let derivation = Composer::derive( + "l64.example.derivation", + "compose", + source.seal(), + result.seal(), + ); + + println!("source: {}", source.pretty()); + println!("result: {}", result.pretty()); + println!( + "derived: {}", + derivation.pretty("l64.example.derivation ∷ compose") + ); + for change in source.seal().explain_changes(result.seal()) { + println!( + "{} ({}) {:016x} -> {:016x}", + change.axis.symbol(), + change.axis.role(), + change.before, + change.after + ); + } +} diff --git a/l64-symbolic/src/lib.rs b/l64-symbolic/src/lib.rs new file mode 100644 index 0000000..9cefb1e --- /dev/null +++ b/l64-symbolic/src/lib.rs @@ -0,0 +1,447 @@ +#![forbid(unsafe_code)] + +use core::{fmt, str::FromStr}; + +const MODULUS: u64 = 18_446_744_073_709_551_557; +const BASE_SIGMA: u64 = 1_099_511_628_211; +const BASE_PI: u64 = 1_469_598_103_934_665_603; +const BASE_DELTA: u64 = 2_305_843_009_213_693_951; +const BASE_OMEGA: u64 = 6_364_136_223_846_793_005; +const IDENTITY_MAGIC: &[u8; 5] = b"L64S1"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum SealAxis { + Sigma, + Pi, + Delta, + Omega, +} + +impl SealAxis { + pub const ALL: [Self; 4] = [Self::Sigma, Self::Pi, Self::Delta, Self::Omega]; + + pub const fn symbol(self) -> &'static str { + match self { + Self::Sigma => "Σ", + Self::Pi => "Π", + Self::Delta => "Δ", + Self::Omega => "Ω", + } + } + + pub const fn role(self) -> &'static str { + match self { + Self::Sigma => "aggregate content mass", + Self::Pi => "ordered composition", + Self::Delta => "adjacent transition structure", + Self::Omega => "nonlinear boundary closure", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AxisChange { + pub axis: SealAxis, + pub before: u64, + pub after: u64, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SymbolicSeal { + axes: [u64; 4], +} + +impl SymbolicSeal { + pub const ZERO: Self = Self { axes: [0; 4] }; + + pub const fn from_axes(sigma: u64, pi: u64, delta: u64, omega: u64) -> Self { + Self { + axes: [sigma, pi, delta, omega], + } + } + + pub const fn axes(self) -> [u64; 4] { + self.axes + } + + pub const fn sigma(self) -> u64 { + self.axes[0] + } + + pub const fn pi(self) -> u64 { + self.axes[1] + } + + pub const fn delta(self) -> u64 { + self.axes[2] + } + + pub const fn omega(self) -> u64 { + self.axes[3] + } + + pub fn to_bytes(self) -> [u8; 32] { + let mut out = [0_u8; 32]; + for (index, axis) in self.axes.into_iter().enumerate() { + out[index * 8..index * 8 + 8].copy_from_slice(&axis.to_le_bytes()); + } + out + } + + pub fn from_bytes(bytes: [u8; 32]) -> Self { + let mut axes = [0_u64; 4]; + for (index, axis) in axes.iter_mut().enumerate() { + *axis = u64::from_le_bytes( + bytes[index * 8..index * 8 + 8] + .try_into() + .expect("symbolic seal axis width"), + ); + } + Self { axes } + } + + pub fn pretty(self, domain: &str) -> String { + format!( + "⟦{domain} ∷ Σ{:016x} ⊗ Π{:016x} ⊗ Δ{:016x} ⊗ Ω{:016x}⟧", + self.sigma(), + self.pi(), + self.delta(), + self.omega() + ) + } + + pub fn changed_axes(self, other: Self) -> Vec { + SealAxis::ALL + .into_iter() + .zip(self.axes.into_iter().zip(other.axes)) + .filter_map(|(axis, (left, right))| (left != right).then_some(axis)) + .collect() + } + + pub fn explain_changes(self, other: Self) -> Vec { + SealAxis::ALL + .into_iter() + .zip(self.axes.into_iter().zip(other.axes)) + .filter_map(|(axis, (before, after))| { + (before != after).then_some(AxisChange { + axis, + before, + after, + }) + }) + .collect() + } + + pub fn fast_matches(self, other: Self) -> bool { + self == other + } +} + +impl fmt::Display for SymbolicSeal { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "s1:{:016x}.{:016x}.{:016x}.{:016x}", + self.sigma(), + self.pi(), + self.delta(), + self.omega() + ) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ParseSealError; + +impl FromStr for SymbolicSeal { + type Err = ParseSealError; + + fn from_str(value: &str) -> Result { + let body = value.strip_prefix("s1:").ok_or(ParseSealError)?; + let mut parts = body.split('.'); + let mut axes = [0_u64; 4]; + for axis in &mut axes { + let part = parts.next().ok_or(ParseSealError)?; + if part.len() != 16 { + return Err(ParseSealError); + } + *axis = u64::from_str_radix(part, 16).map_err(|_| ParseSealError)?; + } + if parts.next().is_some() { + return Err(ParseSealError); + } + Ok(Self { axes }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SymbolicIdentity { + domain: Box, + canonical: Box<[u8]>, +} + +impl SymbolicIdentity { + pub fn new(domain: impl Into>, canonical: impl Into>) -> Self { + Self { + domain: domain.into(), + canonical: canonical.into(), + } + } + + pub fn domain(&self) -> &str { + &self.domain + } + + pub fn canonical(&self) -> &[u8] { + &self.canonical + } + + pub fn seal(&self) -> SymbolicSeal { + let mut composer = Composer::new(&self.domain); + composer.field("canonical", &self.canonical); + composer.finish() + } + + pub fn verify_exact(&self, domain: &str, canonical: &[u8]) -> bool { + self.domain.as_ref() == domain && self.canonical.as_ref() == canonical + } + + pub fn to_exact_bytes(&self) -> Vec { + let domain_len: u32 = self + .domain + .len() + .try_into() + .expect("symbolic identity domain exceeds u32"); + let canonical_len: u64 = self + .canonical + .len() + .try_into() + .expect("symbolic identity payload exceeds u64"); + let mut out = Vec::with_capacity( + IDENTITY_MAGIC.len() + 4 + 8 + self.domain.len() + self.canonical.len(), + ); + out.extend_from_slice(IDENTITY_MAGIC); + out.extend_from_slice(&domain_len.to_le_bytes()); + out.extend_from_slice(&canonical_len.to_le_bytes()); + out.extend_from_slice(self.domain.as_bytes()); + out.extend_from_slice(&self.canonical); + out + } + + pub fn from_exact_bytes(bytes: &[u8]) -> Result { + const HEADER: usize = 5 + 4 + 8; + if bytes.len() < HEADER { + return Err(IdentityDecodeError::Truncated); + } + if &bytes[..5] != IDENTITY_MAGIC { + return Err(IdentityDecodeError::BadMagic); + } + let domain_len = u32::from_le_bytes( + bytes[5..9] + .try_into() + .map_err(|_| IdentityDecodeError::Truncated)?, + ) as usize; + let canonical_len = u64::from_le_bytes( + bytes[9..17] + .try_into() + .map_err(|_| IdentityDecodeError::Truncated)?, + ); + let canonical_len: usize = canonical_len + .try_into() + .map_err(|_| IdentityDecodeError::LengthOverflow)?; + let domain_end = HEADER + .checked_add(domain_len) + .ok_or(IdentityDecodeError::LengthOverflow)?; + let expected = domain_end + .checked_add(canonical_len) + .ok_or(IdentityDecodeError::LengthOverflow)?; + if bytes.len() < expected { + return Err(IdentityDecodeError::Truncated); + } + if bytes.len() > expected { + return Err(IdentityDecodeError::TrailingBytes); + } + let domain = core::str::from_utf8(&bytes[HEADER..domain_end]) + .map_err(|_| IdentityDecodeError::InvalidDomain)?; + Ok(Self::new(domain, &bytes[domain_end..])) + } + + pub fn pretty(&self) -> String { + format!( + "{} ∣ |exact|={}B", + self.seal().pretty(&self.domain), + self.canonical.len() + ) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IdentityDecodeError { + Truncated, + BadMagic, + InvalidDomain, + LengthOverflow, + TrailingBytes, +} + +pub fn exact_identity(domain: &str, canonical: &[u8]) -> SymbolicIdentity { + SymbolicIdentity::new(domain, canonical) +} + +pub fn seal_bytes(domain: &str, canonical: &[u8]) -> SymbolicSeal { + exact_identity(domain, canonical).seal() +} + +#[derive(Debug, Clone)] +pub struct Composer { + axes: [u64; 4], + position: u64, + previous: u64, + segments: u64, +} + +impl Composer { + pub fn new(domain: &str) -> Self { + let mut composer = Self { + axes: [ + 0x5359_4d42_4f4c_4943, + 0x4c36_3453_4541_4c31, + 0x4f52_4445_5245_4431, + 0x424f_554e_4441_5259, + ], + position: 0, + previous: 0, + segments: 0, + }; + composer.boundary(0x01); + composer.raw(domain.as_bytes()); + composer.boundary(0x02); + composer + } + + pub fn field(&mut self, label: &str, bytes: &[u8]) -> &mut Self { + self.boundary(0x10); + self.length(label.len()); + self.raw(label.as_bytes()); + self.boundary(0x11); + self.length(bytes.len()); + self.raw(bytes); + self.boundary(0x12); + self + } + + pub fn u64(&mut self, label: &str, value: u64) -> &mut Self { + self.field(label, &value.to_le_bytes()) + } + + pub fn u32(&mut self, label: &str, value: u32) -> &mut Self { + self.field(label, &value.to_le_bytes()) + } + + pub fn u16(&mut self, label: &str, value: u16) -> &mut Self { + self.field(label, &value.to_le_bytes()) + } + + pub fn u8(&mut self, label: &str, value: u8) -> &mut Self { + self.field(label, &[value]) + } + + pub fn ordered(&mut self, label: &str, parts: &[SymbolicSeal]) -> &mut Self { + self.boundary(0x20); + self.length(label.len()); + self.raw(label.as_bytes()); + self.length(parts.len()); + for part in parts { + self.boundary(0x21); + self.raw(&part.to_bytes()); + } + self.boundary(0x22); + self + } + + pub fn commutative(&mut self, label: &str, parts: &[SymbolicSeal]) -> &mut Self { + let mut sorted = parts.to_vec(); + sorted.sort(); + self.boundary(0x30); + self.length(label.len()); + self.raw(label.as_bytes()); + self.length(sorted.len()); + for part in sorted { + self.boundary(0x31); + self.raw(&part.to_bytes()); + } + self.boundary(0x32); + self + } + + pub fn derive( + domain: &str, + relation: &str, + source: SymbolicSeal, + result: SymbolicSeal, + ) -> SymbolicSeal { + let mut composer = Self::new(domain); + composer.field("relation", relation.as_bytes()); + composer.ordered("derivation", &[source, result]); + composer.finish() + } + + pub fn finish(mut self) -> SymbolicSeal { + self.boundary(0xff); + self.u64("length", self.position); + self.u64("segments", self.segments); + SymbolicSeal { axes: self.axes } + } + + fn length(&mut self, value: usize) { + self.raw(&(value as u64).to_le_bytes()); + } + + fn boundary(&mut self, marker: u8) { + self.segments = self.segments.wrapping_add(1); + self.absorb(u64::from(marker) + 0x100); + } + + fn raw(&mut self, bytes: &[u8]) { + for &byte in bytes { + self.absorb(u64::from(byte) + 1); + } + } + + fn absorb(&mut self, token: u64) { + let index = self.position.wrapping_add(1); + let sigma_term = mul_mod(token, add_mod(index, BASE_SIGMA)); + self.axes[0] = add_mod(self.axes[0], sigma_term); + + self.axes[1] = mul_mod(add_mod(self.axes[1], token), BASE_PI); + + let transition = if self.position == 0 { + token + } else if token >= self.previous { + token - self.previous + } else { + MODULUS - (self.previous - token) + }; + self.axes[2] = add_mod( + mul_mod(self.axes[2], BASE_DELTA), + mul_mod(transition, transition), + ); + + let omega_input = add_mod(add_mod(self.axes[3], token), index); + self.axes[3] = add_mod( + mul_mod(mul_mod(omega_input, omega_input), BASE_OMEGA), + self.segments % MODULUS, + ); + + self.previous = token; + self.position = index; + } +} + +fn add_mod(left: u64, right: u64) -> u64 { + ((u128::from(left) + u128::from(right)) % u128::from(MODULUS)) as u64 +} + +fn mul_mod(left: u64, right: u64) -> u64 { + ((u128::from(left) * u128::from(right)) % u128::from(MODULUS)) as u64 +} diff --git a/l64-symbolic/tests/algebra.rs b/l64-symbolic/tests/algebra.rs new file mode 100644 index 0000000..8476b10 --- /dev/null +++ b/l64-symbolic/tests/algebra.rs @@ -0,0 +1,116 @@ +use l64_symbolic::{ + Composer, SealAxis, SymbolicIdentity, SymbolicSeal, exact_identity, seal_bytes, +}; +use std::collections::BTreeSet; + +fn atom(domain: &str, label: &str, bytes: &[u8]) -> SymbolicSeal { + let mut composer = Composer::new(domain); + composer.field(label, bytes); + composer.finish() +} + +#[test] +fn exact_identity_is_collision_free_by_representation() { + let identity = SymbolicIdentity::new("l64.test", b"abc".as_slice()); + assert!(identity.verify_exact("l64.test", b"abc")); + assert!(!identity.verify_exact("l64.test", b"abd")); + assert!(!identity.verify_exact("l64.other", b"abc")); + assert_eq!( + SymbolicIdentity::from_exact_bytes(&identity.to_exact_bytes()).unwrap(), + identity + ); +} + +#[test] +fn domains_are_separated() { + assert_ne!( + atom("left", "payload", b"same"), + atom("right", "payload", b"same") + ); +} + +#[test] +fn ordered_composition_preserves_order() { + let a = atom("leaf", "a", b"a"); + let b = atom("leaf", "b", b"b"); + let mut left = Composer::new("ordered"); + left.ordered("parts", &[a, b]); + let mut right = Composer::new("ordered"); + right.ordered("parts", &[b, a]); + assert_ne!(left.finish(), right.finish()); +} + +#[test] +fn commutative_composition_normalizes_order() { + let a = atom("leaf", "a", b"a"); + let b = atom("leaf", "b", b"b"); + let mut left = Composer::new("set"); + left.commutative("parts", &[a, b]); + let mut right = Composer::new("set"); + right.commutative("parts", &[b, a]); + assert_eq!(left.finish(), right.finish()); +} + +#[test] +fn canonical_text_round_trips() { + let seal = atom("roundtrip", "payload", b"hello"); + assert_eq!(seal.to_string().parse::().unwrap(), seal); + assert_eq!(SymbolicSeal::from_bytes(seal.to_bytes()), seal); +} + +#[test] +fn axis_changes_are_explainable() { + let left = atom("diff", "payload", b"abc"); + let right = atom("diff", "payload", b"abd"); + let axes = left.changed_axes(right); + assert!(!axes.is_empty()); + assert!(axes.contains(&SealAxis::Sigma)); + let changes = left.explain_changes(right); + assert_eq!(changes.len(), axes.len()); + assert!(changes.iter().all(|change| change.before != change.after)); + assert!(left.pretty("diff").contains('Σ')); + assert!(left.pretty("diff").contains('Ω')); +} + +#[test] +fn small_input_space_has_no_observed_full_seal_collision() { + let mut seen = BTreeSet::new(); + for value in 0_u16..=u16::MAX { + let seal = atom("exhaustive-u16", "value", &value.to_le_bytes()); + assert!(seen.insert(seal), "collision at {value}"); + } +} + +#[test] +fn convenience_helpers_preserve_exact_and_compact_layers() { + let identity = exact_identity("l64.easy", b"payload"); + let seal = seal_bytes("l64.easy", b"payload"); + assert_eq!(identity.seal(), seal); + assert!(identity.verify_exact("l64.easy", b"payload")); + assert!(seal.fast_matches(identity.seal())); +} + +#[test] +fn axis_law_is_typed_and_self_describing() { + let roles = SealAxis::ALL.map(|axis| (axis.symbol(), axis.role())); + assert_eq!(roles[0], ("Σ", "aggregate content mass")); + assert_eq!(roles[1], ("Π", "ordered composition")); + assert_eq!(roles[2], ("Δ", "adjacent transition structure")); + assert_eq!(roles[3], ("Ω", "nonlinear boundary closure")); +} + +#[test] +fn exact_envelope_is_self_delimiting() { + let identity = exact_identity("l64.envelope", b"payload"); + let mut encoded = identity.to_exact_bytes(); + assert_eq!( + SymbolicIdentity::from_exact_bytes(&encoded).unwrap(), + identity + ); + + encoded.push(0); + assert!(SymbolicIdentity::from_exact_bytes(&encoded).is_err()); + encoded.pop(); + encoded[0] ^= 1; + assert!(SymbolicIdentity::from_exact_bytes(&encoded).is_err()); +} diff --git a/l64-symbolic/tests/architecture.rs b/l64-symbolic/tests/architecture.rs new file mode 100644 index 0000000..336c4db --- /dev/null +++ b/l64-symbolic/tests/architecture.rs @@ -0,0 +1,26 @@ +use core::mem::size_of; +use l64_symbolic::SymbolicSeal; +use std::{fs, path::Path}; + +#[test] +fn symbolic_seal_is_exactly_one_fixed_width_frame_field() { + assert_eq!(size_of::(), 32); + assert_eq!(SymbolicSeal::ZERO.to_bytes(), [0; 32]); +} + +#[test] +fn symbolic_carrier_has_no_external_or_cryptographic_dependency() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let manifest = fs::read_to_string(root.join("Cargo.toml")).unwrap(); + assert!(!manifest.contains("[dependencies]")); + + let source = fs::read_to_string(root.join("src/lib.rs")).unwrap(); + for forbidden in [ + ["blake", "3"].concat(), + ["sha", "2"].concat(), + ["serde", "::"].concat(), + ["unsafe", " {"].concat(), + ] { + assert!(!source.contains(&forbidden), "forbidden token: {forbidden}"); + } +}