Add Rust typed Odin core

This commit is contained in:
Meta Vens 2026-06-03 01:39:18 +01:00
commit 24b2e5c2bc
12 changed files with 2030 additions and 0 deletions

1
.gitignore vendored
View file

@ -2,5 +2,6 @@ node_modules/
scratch/
bin/
obj/
target/
*.log
*.pid

1243
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

11
Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[workspace]
members = [
"crates/odin-core",
]
resolver = "3"
[workspace.package]
edition = "2024"
license = "MIT"
publish = false

View file

@ -9,6 +9,17 @@ durable service state in CultCache `.cc`, local Verse visibility through
CultMesh, meaningful dashboards as Eve GUI/TUI DSL, and renderers as lowerers
only.
## Rust Spine
The target Odin body is Rust-first: ingest through narrow ports, normalize into
typed Odin records, persist through CultCache `.cc`, expose through CultMesh /
CultNet document registries, and lower interface state through Eve/CultUI.
The first Rust core lives in `crates/odin-core` and already separates typed
documents, ingest ports, normalization, and repository persistence so unit tests
can use mocked inputs and pipeline smokes can prove typed handoff without
booting the whole daemon.
## Gjallar
Gjallar is Odin's herald organ: the daemon/persona package that should carry

View file

@ -0,0 +1,21 @@
[package]
name = "odin-core"
version = "0.1.0"
edition.workspace = true
license.workspace = true
publish.workspace = true
[lib]
name = "odin_core"
path = "src/lib.rs"
[dependencies]
anyhow = "1"
cultcache-rs = { path = "E:/Projects/EpiphanyAgent/vendor/cultcache-rs" }
cultmesh-rs = { path = "E:/Projects/EpiphanyAgent/vendor/cultmesh-rs" }
cultnet-rs = { path = "E:/Projects/EpiphanyAgent/vendor/cultnet-rs" }
serde = { version = "1", features = ["derive"] }
[dev-dependencies]
pretty_assertions = "1.4"
tempfile = "3"

View file

@ -0,0 +1,144 @@
use cultcache_rs::DatabaseEntry;
pub const ODIN_SNAPSHOT_SCHEMA: &str = "odin.snapshot.v1";
pub const ODIN_VERSE_SCHEMA: &str = "odin.verse.v1";
pub const ODIN_SERVICE_SCHEMA: &str = "odin.service.v1";
pub const ODIN_INTERFACE_SCHEMA: &str = "odin.interface.v1";
pub const ODIN_OBSERVATION_STREAM_SCHEMA: &str = "odin.observation_stream.v1";
pub const ODIN_TRANSLATION_ROUTE_SCHEMA: &str = "odin.translation_route.v1";
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "odin.snapshot", schema = "odin.snapshot.v1")]
pub struct OdinSnapshotRecord {
#[cultcache(key = 0)]
pub snapshot_id: String,
#[cultcache(key = 1)]
pub observed_at: String,
#[cultcache(key = 2)]
pub verse_count: u32,
#[cultcache(key = 3)]
pub service_count: u32,
#[cultcache(key = 4)]
pub interface_count: u32,
#[cultcache(key = 5)]
pub observation_stream_count: u32,
#[cultcache(key = 6)]
pub source: String,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "odin.verse", schema = "odin.verse.v1")]
pub struct OdinVerseRecord {
#[cultcache(key = 0)]
pub verse_id: String,
#[cultcache(key = 1)]
pub name: String,
#[cultcache(key = 2)]
pub role: String,
#[cultcache(key = 3)]
pub status: String,
#[cultcache(key = 4)]
pub capabilities: Vec<String>,
#[cultcache(key = 5)]
pub observed_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "odin.service", schema = "odin.service.v1")]
pub struct OdinServiceRecord {
#[cultcache(key = 0)]
pub service_id: String,
#[cultcache(key = 1)]
pub verse_id: String,
#[cultcache(key = 2)]
pub name: String,
#[cultcache(key = 3)]
pub state: String,
#[cultcache(key = 4)]
pub detail: String,
#[cultcache(key = 5)]
pub authority: String,
#[cultcache(key = 6)]
pub observed_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "odin.interface", schema = "odin.interface.v1")]
pub struct OdinInterfaceRecord {
#[cultcache(key = 0)]
pub provider_id: String,
#[cultcache(key = 1)]
pub title: String,
#[cultcache(key = 2)]
pub state: String,
#[cultcache(key = 3)]
pub source: String,
#[cultcache(key = 4)]
pub version: Option<String>,
#[cultcache(key = 5)]
pub updated_at: Option<String>,
#[cultcache(key = 6)]
pub observed_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "odin.observation_stream",
schema = "odin.observation_stream.v1"
)]
pub struct OdinObservationStreamRecord {
#[cultcache(key = 0)]
pub stream_key: String,
#[cultcache(key = 1)]
pub device_id: String,
#[cultcache(key = 2)]
pub stream_id: String,
#[cultcache(key = 3)]
pub kind: String,
#[cultcache(key = 4)]
pub state: String,
#[cultcache(key = 5)]
pub detail: String,
#[cultcache(key = 6)]
pub owner: String,
#[cultcache(key = 7)]
pub observed_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "odin.translation_route", schema = "odin.translation_route.v1")]
pub struct OdinTranslationRouteRecord {
#[cultcache(key = 0)]
pub route_id: String,
#[cultcache(key = 1)]
pub source_schema: String,
#[cultcache(key = 2)]
pub target_schema: String,
#[cultcache(key = 3)]
pub translation_kind: String,
#[cultcache(key = 4)]
pub owner: String,
#[cultcache(key = 5)]
pub version: String,
#[cultcache(key = 6)]
pub notes: String,
}
cultmesh_rs::cultmesh_documents!(OdinDocuments {
OdinSnapshotRecord => ODIN_SNAPSHOT_SCHEMA,
OdinVerseRecord => ODIN_VERSE_SCHEMA,
OdinServiceRecord => ODIN_SERVICE_SCHEMA,
OdinInterfaceRecord => ODIN_INTERFACE_SCHEMA,
OdinObservationStreamRecord => ODIN_OBSERVATION_STREAM_SCHEMA,
OdinTranslationRouteRecord => ODIN_TRANSLATION_ROUTE_SCHEMA,
});
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct OdinRecords {
pub snapshot: Option<OdinSnapshotRecord>,
pub verses: Vec<OdinVerseRecord>,
pub services: Vec<OdinServiceRecord>,
pub interfaces: Vec<OdinInterfaceRecord>,
pub observation_streams: Vec<OdinObservationStreamRecord>,
pub translation_routes: Vec<OdinTranslationRouteRecord>,
}

View file

@ -0,0 +1,9 @@
pub mod documents;
pub mod pipeline;
pub mod ports;
pub mod repository;
pub use documents::*;
pub use pipeline::*;
pub use ports::*;
pub use repository::*;

View file

@ -0,0 +1,260 @@
use crate::documents::{
OdinInterfaceRecord, OdinObservationStreamRecord, OdinRecords, OdinServiceRecord,
OdinSnapshotRecord, OdinTranslationRouteRecord, OdinVerseRecord,
};
use crate::ports::{
Clock, InterfaceIngest, ObservationStreamIngest, OdinInputBatch, ServiceIngest,
TranslationRouteIngest, VerseIngest,
};
use anyhow::Result;
pub struct OdinIngestPipeline<V, S, I, O, T, C> {
verses: V,
services: S,
interfaces: I,
streams: O,
translations: T,
clock: C,
}
impl<V, S, I, O, T, C> OdinIngestPipeline<V, S, I, O, T, C>
where
V: VerseIngest,
S: ServiceIngest,
I: InterfaceIngest,
O: ObservationStreamIngest,
T: TranslationRouteIngest,
C: Clock,
{
pub fn new(
verses: V,
services: S,
interfaces: I,
streams: O,
translations: T,
clock: C,
) -> Self {
Self {
verses,
services,
interfaces,
streams,
translations,
clock,
}
}
pub fn collect(&self) -> Result<OdinInputBatch> {
Ok(OdinInputBatch {
observed_at: self.clock.now(),
source: "odin.rust.ingest".to_string(),
verses: self.verses.observe_verses()?,
services: self.services.observe_services()?,
interfaces: self.interfaces.observe_interfaces()?,
observation_streams: self.streams.observe_streams()?,
translation_routes: self.translations.observe_translation_routes()?,
})
}
}
pub fn normalize_odin_records(input: OdinInputBatch) -> OdinRecords {
let verses = input
.verses
.into_iter()
.map(|entry| OdinVerseRecord {
verse_id: entry.verse_id,
name: entry.name,
role: entry.role,
status: entry.status,
capabilities: entry.capabilities,
observed_at: input.observed_at.clone(),
})
.collect::<Vec<_>>();
let services = input
.services
.into_iter()
.map(|entry| OdinServiceRecord {
service_id: entry.service_id,
verse_id: entry.verse_id,
name: entry.name,
state: entry.state,
detail: entry.detail,
authority: entry.authority,
observed_at: input.observed_at.clone(),
})
.collect::<Vec<_>>();
let interfaces = input
.interfaces
.into_iter()
.map(|entry| OdinInterfaceRecord {
provider_id: entry.provider_id,
title: entry.title,
state: entry.state,
source: entry.source,
version: entry.version,
updated_at: entry.updated_at,
observed_at: input.observed_at.clone(),
})
.collect::<Vec<_>>();
let observation_streams = input
.observation_streams
.into_iter()
.map(|entry| {
let stream_key = format!("{}:{}:{}", entry.device_id, entry.stream_id, entry.kind);
OdinObservationStreamRecord {
stream_key,
device_id: entry.device_id,
stream_id: entry.stream_id,
kind: entry.kind,
state: entry.state,
detail: entry.detail,
owner: entry.owner,
observed_at: input.observed_at.clone(),
}
})
.collect::<Vec<_>>();
let translation_routes = input
.translation_routes
.into_iter()
.map(|entry| {
let route_id = format!("{}=>{}", entry.source_schema, entry.target_schema);
OdinTranslationRouteRecord {
route_id,
source_schema: entry.source_schema,
target_schema: entry.target_schema,
translation_kind: entry.translation_kind,
owner: entry.owner,
version: entry.version,
notes: entry.notes,
}
})
.collect::<Vec<_>>();
let snapshot = OdinSnapshotRecord {
snapshot_id: "latest".to_string(),
observed_at: input.observed_at,
verse_count: verses.len() as u32,
service_count: services.len() as u32,
interface_count: interfaces.len() as u32,
observation_stream_count: observation_streams.len() as u32,
source: input.source,
};
OdinRecords {
snapshot: Some(snapshot),
verses,
services,
interfaces,
observation_streams,
translation_routes,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ports::{
FixedClock, InterfaceObservation, ObservationStreamObservation, ServiceObservation,
TranslationRouteObservation, VerseObservation,
};
use anyhow::Result;
use pretty_assertions::assert_eq;
#[derive(Clone, Debug, Default)]
struct MockIngest;
impl VerseIngest for MockIngest {
fn observe_verses(&self) -> Result<Vec<VerseObservation>> {
Ok(vec![VerseObservation {
verse_id: "starfire.local".to_string(),
name: "Starfire".to_string(),
role: "coordinator".to_string(),
status: "active".to_string(),
capabilities: vec!["cultmesh".to_string()],
}])
}
}
impl ServiceIngest for MockIngest {
fn observe_services(&self) -> Result<Vec<ServiceObservation>> {
Ok(vec![ServiceObservation {
service_id: "odin".to_string(),
verse_id: "starfire.local".to_string(),
name: "Odin".to_string(),
state: "active".to_string(),
detail: "typed state".to_string(),
authority: "odin".to_string(),
}])
}
}
impl InterfaceIngest for MockIngest {
fn observe_interfaces(&self) -> Result<Vec<InterfaceObservation>> {
Ok(vec![InterfaceObservation {
provider_id: "odin.allseer".to_string(),
title: "Odin".to_string(),
state: "active".to_string(),
source: "cultmesh".to_string(),
version: Some("1".to_string()),
updated_at: None,
}])
}
}
impl ObservationStreamIngest for MockIngest {
fn observe_streams(&self) -> Result<Vec<ObservationStreamObservation>> {
Ok(vec![ObservationStreamObservation {
device_id: "periwinkle".to_string(),
stream_id: "motion".to_string(),
kind: "sensor".to_string(),
state: "active".to_string(),
detail: "fresh".to_string(),
owner: "mimir".to_string(),
}])
}
}
impl TranslationRouteIngest for MockIngest {
fn observe_translation_routes(&self) -> Result<Vec<TranslationRouteObservation>> {
Ok(vec![TranslationRouteObservation {
source_schema: "mimir.eve_sensor_observation.v1".to_string(),
target_schema: "odin.observation_stream.v1".to_string(),
translation_kind: "projection".to_string(),
owner: "odin".to_string(),
version: "v1".to_string(),
notes: "dashboard projection".to_string(),
}])
}
}
#[test]
fn pipeline_collects_from_injected_ports() -> Result<()> {
let pipeline = OdinIngestPipeline::new(
MockIngest,
MockIngest,
MockIngest,
MockIngest,
MockIngest,
FixedClock::new("2026-06-03T00:00:00Z"),
);
let records = normalize_odin_records(pipeline.collect()?);
assert_eq!(records.snapshot.unwrap().service_count, 1);
assert_eq!(records.verses[0].verse_id, "starfire.local");
assert_eq!(records.services[0].authority, "odin");
assert_eq!(
records.observation_streams[0].stream_key,
"periwinkle:motion:sensor"
);
assert_eq!(
records.translation_routes[0].route_id,
"mimir.eve_sensor_observation.v1=>odin.observation_stream.v1"
);
Ok(())
}
}

View file

@ -0,0 +1,102 @@
use anyhow::Result;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VerseObservation {
pub verse_id: String,
pub name: String,
pub role: String,
pub status: String,
pub capabilities: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ServiceObservation {
pub service_id: String,
pub verse_id: String,
pub name: String,
pub state: String,
pub detail: String,
pub authority: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InterfaceObservation {
pub provider_id: String,
pub title: String,
pub state: String,
pub source: String,
pub version: Option<String>,
pub updated_at: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ObservationStreamObservation {
pub device_id: String,
pub stream_id: String,
pub kind: String,
pub state: String,
pub detail: String,
pub owner: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TranslationRouteObservation {
pub source_schema: String,
pub target_schema: String,
pub translation_kind: String,
pub owner: String,
pub version: String,
pub notes: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct OdinInputBatch {
pub observed_at: String,
pub source: String,
pub verses: Vec<VerseObservation>,
pub services: Vec<ServiceObservation>,
pub interfaces: Vec<InterfaceObservation>,
pub observation_streams: Vec<ObservationStreamObservation>,
pub translation_routes: Vec<TranslationRouteObservation>,
}
pub trait VerseIngest {
fn observe_verses(&self) -> Result<Vec<VerseObservation>>;
}
pub trait ServiceIngest {
fn observe_services(&self) -> Result<Vec<ServiceObservation>>;
}
pub trait InterfaceIngest {
fn observe_interfaces(&self) -> Result<Vec<InterfaceObservation>>;
}
pub trait ObservationStreamIngest {
fn observe_streams(&self) -> Result<Vec<ObservationStreamObservation>>;
}
pub trait TranslationRouteIngest {
fn observe_translation_routes(&self) -> Result<Vec<TranslationRouteObservation>>;
}
pub trait Clock {
fn now(&self) -> String;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FixedClock {
now: String,
}
impl FixedClock {
pub fn new(now: impl Into<String>) -> Self {
Self { now: now.into() }
}
}
impl Clock for FixedClock {
fn now(&self) -> String {
self.now.clone()
}
}

View file

@ -0,0 +1,185 @@
use crate::documents::{
OdinDocuments, OdinRecords, OdinServiceRecord, OdinSnapshotRecord, OdinVerseRecord,
};
use anyhow::Result;
use cultmesh_rs::{CultMesh, CultMeshNode, CultMeshNodeOptions};
use std::path::Path;
pub trait OdinRepository {
fn persist_records(&mut self, records: &OdinRecords) -> Result<()>;
}
pub struct CultMeshOdinRepository {
node: CultMeshNode,
}
impl CultMeshOdinRepository {
pub fn open(store_path: impl AsRef<Path>, runtime_id: impl Into<String>) -> Result<Self> {
let node = CultMesh::create_node(
store_path,
OdinDocuments,
CultMeshNodeOptions {
runtime_id: runtime_id.into(),
pull_on_start: true,
},
)?;
Ok(Self { node })
}
pub fn get_snapshot(&self) -> Result<OdinSnapshotRecord> {
self.node.get_required::<OdinSnapshotRecord>("latest")
}
pub fn get_service(&self, service_id: &str) -> Result<Option<OdinServiceRecord>> {
self.node.get::<OdinServiceRecord>(service_id)
}
pub fn get_verse(&self, verse_id: &str) -> Result<Option<OdinVerseRecord>> {
self.node.get::<OdinVerseRecord>(verse_id)
}
pub fn document_binding_version(&self, document_type: &str) -> Option<String> {
self.node
.documents()
.binding(document_type)
.and_then(|binding| binding.payload_schema_version.clone())
}
}
impl OdinRepository for CultMeshOdinRepository {
fn persist_records(&mut self, records: &OdinRecords) -> Result<()> {
if let Some(snapshot) = &records.snapshot {
self.node.put("latest", snapshot)?;
}
for verse in &records.verses {
self.node.put(&verse.verse_id, verse)?;
}
for service in &records.services {
self.node.put(&service.service_id, service)?;
}
for interface in &records.interfaces {
self.node.put(&interface.provider_id, interface)?;
}
for stream in &records.observation_streams {
self.node.put(&stream.stream_key, stream)?;
}
for route in &records.translation_routes {
self.node.put(&route.route_id, route)?;
}
Ok(())
}
}
#[derive(Default)]
pub struct InMemoryOdinRepository {
pub records: OdinRecords,
}
impl OdinRepository for InMemoryOdinRepository {
fn persist_records(&mut self, records: &OdinRecords) -> Result<()> {
self.records = records.clone();
Ok(())
}
}
pub fn persist_pipeline_records(
repository: &mut impl OdinRepository,
records: OdinRecords,
) -> Result<OdinRecords> {
repository.persist_records(&records)?;
Ok(records)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::documents::ODIN_SERVICE_SCHEMA;
use crate::pipeline::normalize_odin_records;
use crate::ports::{
InterfaceObservation, ObservationStreamObservation, OdinInputBatch, ServiceObservation,
TranslationRouteObservation, VerseObservation,
};
use pretty_assertions::assert_eq;
fn sample_records() -> OdinRecords {
normalize_odin_records(OdinInputBatch {
observed_at: "2026-06-03T00:00:00Z".to_string(),
source: "unit-test".to_string(),
verses: vec![VerseObservation {
verse_id: "starfire.local".to_string(),
name: "Starfire".to_string(),
role: "coordinator".to_string(),
status: "active".to_string(),
capabilities: vec!["cultmesh".to_string()],
}],
services: vec![ServiceObservation {
service_id: "odin".to_string(),
verse_id: "starfire.local".to_string(),
name: "Odin".to_string(),
state: "active".to_string(),
detail: "persisted".to_string(),
authority: "odin".to_string(),
}],
interfaces: vec![InterfaceObservation {
provider_id: "odin.allseer".to_string(),
title: "Odin".to_string(),
state: "active".to_string(),
source: "cultmesh".to_string(),
version: Some("1".to_string()),
updated_at: None,
}],
observation_streams: vec![ObservationStreamObservation {
device_id: "periwinkle".to_string(),
stream_id: "camera".to_string(),
kind: "media".to_string(),
state: "active".to_string(),
detail: "fresh".to_string(),
owner: "mimir".to_string(),
}],
translation_routes: vec![TranslationRouteObservation {
source_schema: "source.v1".to_string(),
target_schema: "target.v1".to_string(),
translation_kind: "projection".to_string(),
owner: "odin".to_string(),
version: "v1".to_string(),
notes: "test".to_string(),
}],
})
}
#[test]
fn memory_repository_supports_fast_unit_tests() -> Result<()> {
let records = sample_records();
let mut repository = InMemoryOdinRepository::default();
repository.persist_records(&records)?;
assert_eq!(repository.records.services[0].service_id, "odin");
Ok(())
}
#[test]
fn cultmesh_repository_round_trips_typed_records() -> Result<()> {
let temp = tempfile::tempdir()?;
let store_path = temp.path().join("odin.cc");
let records = sample_records();
let mut repository = CultMeshOdinRepository::open(&store_path, "odin-test")?;
repository.persist_records(&records)?;
assert_eq!(
repository
.document_binding_version("odin.service")
.as_deref(),
Some(ODIN_SERVICE_SCHEMA)
);
let reloaded = CultMeshOdinRepository::open(&store_path, "odin-test-reloaded")?;
assert_eq!(reloaded.get_snapshot()?.service_count, 1);
assert_eq!(reloaded.get_service("odin")?.unwrap().detail, "persisted");
assert_eq!(
reloaded.get_verse("starfire.local")?.unwrap().capabilities,
vec!["cultmesh".to_string()]
);
Ok(())
}
}

View file

@ -22,10 +22,39 @@ host / device / service probes
This first path proves the operator surface and persistent state. It does not yet pretend to be full peer exchange.
## Rust Target Spine
The target Odin machine is Rust-first and typed-state-first:
```text
Verse / host / device / provider inputs
-> ingest ports
-> normalization
-> typed Odin records
-> CultMesh node
-> CultCache .cc persistence
-> CultNet/CultMesh document registry
-> Eve/CultUI projection and agent affordance packets
```
The first Rust core lives in `crates/odin-core`:
- `documents.rs`: typed Odin records and the CultMesh document set.
- `ports.rs`: narrow ingest traits plus clock injection for deterministic tests.
- `pipeline.rs`: collection and normalization from input observations to typed records.
- `repository.rs`: `OdinRepository` abstraction, in-memory mock repository, and CultMesh-backed repository.
The Rust spine owns the future architecture. The CommonJS daemon remains the
legacy operational body until each organ crosses this typed boundary.
## Runtime Body
Odin's executable body is split by ownership:
- `crates/odin-core`: Rust target core. Owns typed Odin documents, ingest
ports, normalization, and CultMesh/CultCache repository boundaries. This is
the replacement spine; JavaScript remains legacy runtime/probe scaffolding
until each organ has crossed the typed boundary.
- `src/odin-coordinator.cjs`: process lifecycle, refresh loop, persistence, health, and transport wiring.
- `src/odin/config.cjs`: runtime paths, seed deck URLs, intervals, and CultLib module path setup.
- `src/odin/documents.cjs`: CultCache/CultMesh document definitions accepted by Odin.
@ -68,6 +97,19 @@ Verse announcement
- Renderers lower surfaces only. If a renderer fixes network truth, the machine is split-brained.
- CultCache is the durable state substrate; CultNet is the wire vocabulary; CultMesh is the Verse and peer-consensus layer.
- The Eve surface carries explicit `verse`, `service`, and `observation-stream` nodes. Compact renderers may derive visual facets from those nodes, but may not invent observation truth.
- Rust organs must accept mocked inputs through narrow traits. Unit tests prove
local invariants; pipeline smokes prove adjacent typed handoff; full daemon
boots are not the only test path.
- JSON is not state authority. It is allowed only for schema publication,
debugging, compatibility export, or external xenos boundaries.
## Test Surfaces
Current Rust verification:
- `pipeline_collects_from_injected_ports`: proves ingest ports and clock injection.
- `memory_repository_supports_fast_unit_tests`: proves repository consumers can test without CultMesh.
- `cultmesh_repository_round_trips_typed_records`: proves typed records persist through CultMesh/CultCache and reload from `.cc`.
## Service Architecture Contract

View file

@ -8,6 +8,7 @@
"gjallar:persona": "node scripts/write-gjallar-persona-cc.mjs inspect",
"gjallar:persona:write": "node scripts/write-gjallar-persona-cc.mjs write",
"gjallar:build": "dotnet build src/Gjallar/Gjallar.csproj",
"rust:test": "cargo test --workspace",
"check": "node scripts/check-syntax.cjs",
"start": "node src/odin-coordinator.cjs"
},