Skip to content

Blackfall-Labs/noderalis

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

147 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TERNSIG

Virtual Mainframe Runtime

Version 3.0 — Blackfall Labs


Ternsig is a virtual mainframe.

It does not simulate one. It does not approximate one. It is one — a centralized computational facility that accepts programs, manages persistent state, and provides uniform services to every application connected to it. Programs written in Runes are loaded, parsed, and evaluated against a kernel of typed modules. The mainframe owns the substrate. The mainframe owns the storage. The mainframe owns the execution lifecycle. Consumer applications request services from the mainframe. They do not reimplement them, any more than a payroll program reimplements the operating system.

The kernel is Rust. The programs are .rune files. The division of labor is absolute.


Platform Architecture (v3)

Ternsig v3 introduces a module-driven platform with typed dispatch, lifecycle orchestration, and full system introspection. Applications are built by registering modules — each module declares its capabilities, jobs, routines, hooks, events, and error codes. The platform validates, indexes, and wires everything at boot. At runtime, any component of the system can be queried through the built-in system:spec capability.

┌─────────────────────────────────────────────┐
│             TERNSIG MAINFRAME               │
│                                             │
│  ┌───────────────────────────────────────┐  │
│  │           ServiceEngine               │  │
│  │  module registration · dispatch       │  │
│  │  builtin: system:spec                 │  │
│  └──────────────────┬────────────────────┘  │
│                     │                        │
│  ┌──────────────────┴────────────────────┐  │
│  │         BootOrchestrator              │  │
│  │  Cold → Preflight → Bootstrap         │  │
│  │  → Ignition → Active → Shutdown       │  │
│  └──────────────────┬────────────────────┘  │
│                     │                        │
│  ┌──────────────────┴──┐ ┌───────────────┐  │
│  │  Registered Modules  │ │   Platform    │  │
│  │  · capabilities      │ │   Services    │  │
│  │  · jobs              │ │   · events    │  │
│  │  · routines          │ │   · forms     │  │
│  │  · hooks             │ │   · storage   │  │
│  │  · errors            │ │   · logging   │  │
│  │                      │ │   · tray      │  │
│  └──────────────────────┘ └───────────────┘  │
└─────────────────────────────────────────────┘
                      ▲
                      │
┌─────────────────────┴─────────────────────┐
│           Consumer Application            │
│        (Strategos, Astromind, etc.)        │
│                                           │
│  Implements TernsigModule                 │
│  Dispatches capabilities                  │
│  Runs as a BuiltSystem                    │
└───────────────────────────────────────────┘

All platform traits, the Runes parser, the evaluator, and the full persistence layer are re-exported through the ternsig crate. Consumers require no direct dependency on runes-core, runes-parser, runes-eval, cartridge-rs, engram-rs, databank-rs, or dataspool-rs. One dependency. One interface. One mainframe.


Building an Application

A Ternsig application is a set of modules registered through the builder. Each module is a struct that implements TernsigModule. The builder validates, boots, and returns a BuiltSystem you run on a host target.

use ternsig::builder::{BuilderIpcConfig, Ternsig};
use ternsig::daemon::HostTarget;

fn main() {
    Ternsig::new("my-app")
        .version("1.0.0")
        .ipc(BuilderIpcConfig::tcp(19800))
        .workers(4)
        .module(MyModule)
        .build()
        .expect("boot failed")
        .run(HostTarget::Daemon);
}

Host Targets

A host is an operational container that owns the process lifecycle. The platform (modules, engine, storage, IPC, MCP) is identical across hosts — only the main thread ownership differs.

Host What it does
HostTarget::Daemon Headless background process. IPC, MCP, PID file, lockfile, ctrl-c handler. Event loop blocks forever.
HostTarget::Desktop(config) Tauri tray application. Same IPC/MCP, but Tauri owns the main thread. Requires desktop feature.
// Desktop mode (behind #[cfg(feature = "desktop")])
use ternsig::desktop::DesktopConfig;

system.run(HostTarget::Desktop(DesktopConfig {
    icon_png: include_bytes!("../icons/icon.png"),
    app_name: "My App".into(),
    tray_id: "my-app-tray".into(),
    poll_method: "daemon.status".into(),
    status_fn: Arc::new(|_| "Online".into()),
    sections: vec![],
    tauri_context: ternsig::ternsig_tauri::tauri::generate_context!(),
}));

Both hosts share the same boot sequence (daemon::start()), which acquires the lockfile, sets up PID/logging, registers ctrl-c, and starts IPC/MCP threads. The daemon host then runs a sleep-poll event loop. The desktop host hands the main thread to Tauri.

For testing and introspection without running the event loop, use dispatch() directly:

let system = Ternsig::new("test").data_dir(dir).module(MyModule).build()?;
let result = system.dispatch("my:capability", json!({"key": "value"}));
let spec = system.dispatch("system:spec", json!({}));

See examples/factory-fresh for a complete working application.


Module Trait

Every module implements TernsigModule. All methods except name(), error_prefix(), and error_catalog() have defaults that return empty sets — modules only declare what they use.

use ternsig::traits::*;

struct MyModule;

impl TernsigModule for MyModule {
    fn name(&self) -> &str { "my_module" }
    fn error_prefix(&self) -> &str { "MY" }
    fn error_catalog(&self) -> Box<dyn ErrorCatalog> { Box::new(MyCatalog) }

    fn capabilities(&self) -> CapabilitySet {
        CapabilitySet::new()
            .register(PingCapability)
            .register(CreateCapability)
    }

    fn jobs(&self) -> JobSet {
        JobSet::new()
            .register(ProcessJob)
    }

    fn routines(&self) -> RoutineSet {
        RoutineSet::new()
            .register(DailyCleanup)
    }

    fn hooks(&self) -> HookSet {
        HookSet::new()
            .register(BootstrapHook)
            .register(ShutdownHook)
    }

    fn events(&self) -> Vec<EventDef> {
        vec![EventDef::new::<ItemCreated>("An item was created")]
    }

    fn subscriptions(&self) -> Vec<Subscription> {
        vec![Subscription::on::<ItemCreated>(|event, _ctx| { Ok(()) })]
    }
}

The platform validates at registration time:

  • No duplicate module names
  • No duplicate capability names across all modules
  • No duplicate error prefixes
  • No duplicate job kinds

#[module] Enforcement

The #[module] proc macro enforces platform rules at compile time. It generates shadow macros that make println!, eprintln!, and dbg! into compile errors inside module code — all output must go through spools.

// These are compile errors inside any #[module] crate:
println!("debug info");           // → use ctx.spool().info()
eprintln!("error occurred");      // → use ctx.spool().error()
dbg!(value);                      // → use ctx.spool().debug()

The shadows are emitted at the #[module] expansion site, which by convention is the top of lib.rs. Because macro_rules! definitions are visible forward in the file and into child modules, the enforcement covers the entire module crate — capabilities, jobs, hooks, events, everything.

What the macro enforces at compile time: println!, eprintln!, dbg!

What's forbidden by convention (not compile-enforced): log::*, std::io::stdout/stderr, direct rusqlite::Connection. These are path-based imports that a proc macro cannot intercept. The #[module] macro does not carry clippy lints to downstream crates — only code it emits into the crate is enforceable.

The correct replacement for all output is ctx.spool():

ctx.spool("greeter.audit").info(json!({"action": "greet", "name": name}))?;
ctx.spool("greeter.audit").warn(json!({"note": "empty name"}))?;
ctx.spool("greeter.errors").error("GREET_0001", &[("context", "dispatch")])?;
ctx.spool("greeter.audit").debug(json!({"step": "validation"}))?;

Platform Subsystems

Capabilities

A capability is a typed command handler — the single unit of functionality in the platform. Each capability declares typed Params and Result, metadata for CLI/IPC/MCP projection, and a protection level.

impl TernsigCapability for Ping {
    type Params = PingParams;
    type Result = PingResult;

    fn meta(&self) -> CapabilityMeta {
        CapabilityMeta {
            name: "my:ping",
            path: &["my", "ping"],
            description: "Health check",
            category: "system",
            args: &[],
            protection: Protection::Direct,
        }
    }

    fn handle(&self, params: PingParams, ctx: &Value) -> MainframeResult<PingResult> {
        Ok(PingResult { status: "ok".into() })
    }
}

Protection levels:

  • Protection::Direct — callable immediately via dispatch()
  • Protection::FormProtected — requires the form lifecycle (request → fill → submit). Direct dispatch is rejected. The capability must also implement fn form() -> Option<FormSpec> to provide the CML template.

Jobs

Background work with typed parameters, results, and timeouts. Jobs are dispatched by kind and executed by worker threads.

impl TernsigJob for Extract {
    type Params = ExtractParams;
    type Result = ExtractResult;

    fn kind(&self) -> &str { "extract" }
    fn description(&self) -> &str { "Extract documents" }
    fn timeout(&self) -> Duration { Duration::from_secs(300) }

    fn run(&self, params: ExtractParams, ctx: &Value) -> MainframeResult<ExtractResult> {
        // ...
    }
}

Routines

Scheduled jobs. A routine defines a cron expression and builds job params at fire time — not at registration time.

impl TernsigRoutine for Snapshot {
    type Params = SnapshotParams;

    fn name(&self) -> &str { "snapshot" }
    fn description(&self) -> &str { "Periodic metric snapshot" }
    fn cron(&self) -> &str { "0 */6 * * *" }
    fn enabled_by_default(&self) -> bool { true }

    fn params(&self, ctx: &Value) -> MainframeResult<SnapshotParams> {
        Ok(SnapshotParams { scope: "full".into() })
    }
}

Hooks

Lifecycle hooks run at specific boot phases. Bootstrap hooks run after storage is allocated, in registration order. Shutdown hooks run before databases close, in reverse registration order.

impl TernsigHook for SeedData {
    fn phase(&self) -> Phase { Phase::Bootstrap }

    fn run(&self, ctx: &Value) -> MainframeResult<()> {
        // seed default data, scan for projects, etc.
        Ok(())
    }
}

Events

Typed publish-subscribe for runtime signals. Events are synchronous within the emitting thread. Not lifecycle hooks — events handle runtime occurrences like "an item was created" or "a job completed."

// Define an event
struct ItemCreated { id: u64 }
impl ModuleEvent for ItemCreated {
    fn event_type(&self) -> &'static str { "ItemCreated" }
}

// Declare in module
fn events(&self) -> Vec<EventDef> {
    vec![EventDef::new::<ItemCreated>("An item was created")]
}

// Subscribe in module
fn subscriptions(&self) -> Vec<Subscription> {
    vec![Subscription::on::<ItemCreated>(|event, _ctx| {
        // println! is forbidden — use spools for all output
        Ok(())
    })]
}

Error Catalog

Every module declares its error codes with structured entries — code, title, template, and severity. Templates support {param} substitution.

ErrorEntry {
    code: "MY_0001".into(),
    title: "Not Found".into(),
    template: "Item {id} does not exist".into(),
    severity: ErrorSeverity::Error,
}

The platform aggregates all module catalogs. Lookup by prefix:

if let Some(catalog) = system.engine().error_catalog("MY") {
    let msg = catalog.render("MY_0001", &[("id", "42")]);
    // → Some("Item 42 does not exist")
}

System Introspection

Every Ternsig system includes two built-in capabilities that are always available without module registration:

system:spec — Returns a complete SystemSpec snapshot: all modules, capabilities, jobs, routines, events, error codes, spools, tables, and cartridges.

let result = system.dispatch("system:spec", json!({}));
// result.value contains the full SystemSpec as JSON

system:module_spec — Returns the spec for a single module by name.

let result = system.dispatch("system:module_spec", json!({"module": "greeter"}));

These capabilities self-describe — system:spec output includes system:spec and system:module_spec in its capabilities list.


Boot Lifecycle

The BootOrchestrator drives the system through phases:

Cold → Preflight → Bootstrap → Ignition → Active → ShuttingDown → Shutdown
Phase What happens
Cold Module registration. No storage, no dispatch.
Preflight Validate data directory, verify manifests.
Bootstrap Run bootstrap hooks. Storage allocated, schemas applied.
Ignition Consumer ignition steps, spawn workers.
Active System operational. Dispatch, IPC, MCP, scheduler running.
Shutdown Shutdown hooks (reverse order), close storage.

The builder's .build() runs Cold through Active automatically. .shutdown() reverses.


The Runes Language

Runes is a behavioral scripting language designed for systems that must express what they do in terms a human can read and a machine can enforce. Its syntax descends from Ruby. A Runes program declares its identity, imports the modules it requires, and calls verbs — named operations provided by the mainframe's kernel.

rune "neuromodulator_dynamics" do
  version 1
  namespace :kernel
end

use :chem
use :math

da = read(:dopamine)
if da > 180 do
  inject(:dopamine, negate(shift(da, 3)))
end

The engine enforces namespace isolation. A program declaring namespace :kernel may call verbs from modules registered under kernel, and no others. This is not a convention. It is a gate.


Signal Arithmetic

The fundamental data type across all Blackfall systems occupies three bytes and requires no floating-point unit.

Signal:  s = p × m × k    p ∈ {-1, 0, +1}    m ∈ {0..255}    k ∈ {1..255}
pub struct Signal {
    pub polarity: i8,     // -1, 0, or +1
    pub magnitude: u8,    // 0-255
    pub multiplier: u8,   // scaling factor (default 1)
}

Three values. Three states of polarity. Excitatory, inhibitory, or silent. The arithmetic is integer add and subtract. No GPU dependency. No floating-point dependency.


Mastery Learning

Ternsig ships a pure-integer adaptive learning algorithm. It does not descend from gradient descent. There is no loss function. There is no backpropagation. There is no optimizer.

  • Peak-relative gating — Only activations exceeding one quarter of the peak magnitude participate in updates.
  • Sustained pressure — Pressure must accumulate before the system commits to structural change.
  • Weaken before flip — Magnitude depletes to zero before polarity reversal. No discontinuous jumps.
  • Dopamine gating — Learning requires neuromodulator support above threshold.
  • Temperature lifecycle — HOT learns freely, WARM cautiously, COOL resists, COLD is frozen.

Persistence

All persistent state flows through the storage layer. Four backends, one write-ahead log.

Format Extension Crate Purpose Mutability
Runes Program .rune runes Behavioral programs Read-only at runtime
Cartridge .cart cartridge-rs Mutable runtime state Read-write
Engram .eng engram-rs Immutable knowledge archives Read-only
DataSpool .spool dataspool-rs Structured logs and audit trails Append-only
Databank (in-memory) databank-rs Representational memory Read-write

Project Structure

ternsig/
├── src/
│   ├── lib.rs                  # Crate root, Runes + platform re-exports
│   ├── traits/                 # Platform trait contracts
│   │   ├── module.rs           # TernsigModule, CapabilitySet, JobSet, etc.
│   │   ├── capability.rs       # TernsigCapability, Protection, CapabilityMeta
│   │   ├── job.rs              # TernsigJob, ErasedJob
│   │   ├── routine.rs          # TernsigRoutine, ErasedRoutine
│   │   ├── hook.rs             # TernsigHook, Phase
│   │   ├── event_bus.rs        # ModuleEvent, EventBus
│   │   ├── error_catalog.rs    # ErrorCatalog, ErrorEntry
│   │   ├── cartridge_db.rs     # CartridgeDb, CartridgeModel, TableDef
│   │   ├── form_protocol.rs    # FormProtocol
│   │   ├── spool_handle.rs     # SpoolHandle
│   │   ├── vfs_handle.rs       # VfsHandle
│   │   └── tray_section.rs     # TrayProvider
│   ├── engine.rs               # ServiceEngine — dispatch, registration, builtins
│   ├── builder.rs              # Ternsig builder + BuiltSystem
│   ├── daemon.rs               # Host lifecycle — start/event_loop/cleanup, HostTarget
│   ├── desktop.rs              # Desktop host (Tauri tray, behind "desktop" feature)
│   ├── boot_v3/                # BootOrchestrator — lifecycle phases
│   ├── introspection/          # SystemSpec collector
│   ├── types/                  # Platform types (introspection, manifest, etc.)
│   ├── storage/                # StorageHub, DbHandle, VfsHandle, SpoolHandle
│   ├── events/                 # RuntimeEventBus
│   ├── forms/                  # PlatformFormEngine
│   ├── context/                # ServiceContext, Identity
│   ├── identity/               # Permission model
│   ├── jobs/                   # JobQueue
│   ├── routines/               # Scheduler
│   ├── errors/                 # ErrorRegistry
│   ├── error.rs                # MainframeError, ErrorCode
│   ├── error_catalog.rs        # TernsigErrorCatalog (TSG_XXXX)
│   ├── learning.rs             # Mastery learning algorithm
│   ├── logging.rs              # Dataspool-backed structured logging
│   ├── loader.rs               # .rune file discovery and parsing
│   ├── ternary.rs              # Signal type (re-exports ternary-signal)
│   └── snn/                    # Spiking neural network substrate
├── ternsig-core/               # Shared types (ChemicalState, Tier, etc.)
├── ternsig-cli/                # CLI framework
├── ternsig-macros/             # Proc macros (#[table], #[module])
├── ternsig-daemon/             # Daemon runner
├── ternsig-tauri/              # Tauri desktop integration
├── ternsig-legacy/             # Archived v2 modules (reference only)
├── examples/
│   ├── factory-fresh/          # Complete standalone Ternsig application
│   ├── dopamine_gated_learning.rune
│   ├── metabolic_watchdog.rune
│   └── neuromodulator_homeostasis.rune
├── tests/
│   ├── platform.rs             # 53 integration tests (v3 platform)
│   ├── table_macro.rs          # #[table] proc macro tests
│   └── module_macro.rs         # #[module] proc macro tests
└── Cargo.toml

Workspace Crates

Crate Role
ternsig Platform crate — engine, traits, builder, daemon/desktop hosts, all platform subsystems
ternsig-core Shared types: ChemicalState, SignalTemperature, Tier
ternsig-cli CLI framework (re-exported through ternsig)
ternsig-macros Proc macros: #[table], #[module]
ternsig-daemon Daemon infrastructure — IPC server, MCP stdio, PID management, logging, lockfile, ctrl-c
ternsig-tauri Tauri tray integration — menu sections, polling, icon (optional, behind desktop feature)
ternsig-legacy Archived v2 modules — source reference only, does not compile

License

MIT OR Apache-2.0


Blackfall Labs — Neuromorphic systems. Not machine learning.

About

Virtual mainframe runtime — Runes-based kernel for neuromorphic systems

Topics

Resources

Stars

Watchers

Forks

Sponsor this project

  •  

Packages

 
 
 

Contributors

Languages