diff --git a/.devin/blueprint.yaml b/.devin/blueprint.yaml new file mode 100644 index 0000000..573ab9b --- /dev/null +++ b/.devin/blueprint.yaml @@ -0,0 +1,29 @@ +# Devin environment blueprint for synaptic-mesh. +# +# This file is a version-controlled REFERENCE ONLY. Devin blueprints are not +# auto-read from the repo — copy this content into Devin's +# Settings > Environment > Blueprints > synaptic-mesh UI to apply it. +# Keep this file in sync whenever the blueprint is changed in the UI. + +initialize: | + rustup toolchain install stable --component clippy --component rustfmt + rustup default stable + +maintenance: | + cargo fetch --locked + +knowledge: + - name: format-check + contents: cargo fmt --check + - name: test + contents: cargo test --locked + - name: lint + contents: cargo clippy --all-features -- -D warnings + - name: review-gate + contents: | + Before opening or updating a PR, run every command in REVIEW.md + (format check, locked test matrix, clippy, regression guards, + diff hygiene, origin hygiene). All must pass; REVIEW.md's + "Pass criteria" section is the merge bar. + - name: repo-map + contents: See AGENTS.md for module map, entry order, and design principles. diff --git a/.gitignore b/.gitignore index 210d3f5..af41c19 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ .air/ .opencode/ .cline/ +.ai/ +.junie/ # Isolated Git Worktrees .worktrees/ \ No newline at end of file diff --git a/README.md b/README.md index 3e22184..5deeb0f 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,17 @@ let decision = router.route(&[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0]); - **Neuron State Snapshots** — Per-neuron adaptation and error tracking for dynamic routing decisions. - **Routing Policies** — Configurable scoring equations that balance spike activity, adaptation penalties, and error bonuses. +## Crate Boundary + +`synaptic-mesh` does **not** depend on the separate [`neuromod`](https://github.com/Limen-Neural/neuromod) crate. The two crates are kept independent so each can evolve without coupling: + +| Crate | Owns | +|-------|------| +| **neuromod** | Canonical neuron models — LIF, Izhikevich, Hodgkin-Huxley, GIF, FitzHugh-Nagumo, Lapicque | +| **synaptic-mesh** | Topology, wiring, delay infrastructure, CSR sparse maps, `ChannelRouter`, and its router-internal `NeuromodNeuron` (NIF) integration primitive | + +`NeuromodNeuron` lives in [`router`](src/router.rs) — it's a router-internal integration primitive, not a general-purpose neuron model. + ## Architecture ```text diff --git a/REVIEW.md b/REVIEW.md index be4d6eb..2e0ca56 100644 --- a/REVIEW.md +++ b/REVIEW.md @@ -29,10 +29,10 @@ After any "security" or dependency PR, confirm core product APIs still exist: ```bash # Check for key structs and modules rg -n 'pub struct SynapticMesh' src/mesh.rs -rg -n 'pub struct NeuromodNeuron' src/neuromod.rs +rg -n 'pub struct NeuromodNeuron' src/router.rs rg -n 'pub struct SynapticGraph' src/topology/graph.rs rg -n 'pub mod topology' src/lib.rs -rg -n 'pub mod neuromod' src/lib.rs +rg -n 'pub use router::\{[^}]*NeuromodNeuron' src/lib.rs # confirms the router re-export (not a removed `neuromod` module) ``` ## Diff hygiene @@ -58,7 +58,7 @@ git check-ignore -v .worktrees .idea target ## Do not merge if -- `src/mesh.rs` or `src/neuromod.rs` are unexpectedly altered or removed. +- `src/mesh.rs` or `src/router.rs` are unexpectedly altered or removed. - `git diff origin/main` shows unexpected public-API removals. ## Pass criteria diff --git a/src/lib.rs b/src/lib.rs index f6436cd..35450cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,8 +13,17 @@ //! | [`delay`] | Temporal delay infrastructure — ring-buffer spike queues for tick-aligned delivery with configurable axonal propagation delays | //! | [`mesh`] | [`SynapticMesh`] orchestrator — the top-level struct owning topology + delays, provides `propagate()` for spike → current conversion | //! | [`sparse`] | Compressed Sparse Row (CSR) synaptic maps for GPU-optimized weight matrices | -//! | [`router`] | Generic multi-channel SNN router using neuromodulatory neurons | -//! | [`neuromod`] | Neuromodulatory (NIF) neuron model with gain control | +//! | [`router`] | Generic multi-channel SNN router using [`NeuromodNeuron`], a router-internal NIF integration primitive | +//! +//! ## Crate boundary +//! +//! `synaptic-mesh` does **not** depend on the separate `neuromod` crate. The +//! two crates are kept independent so each can evolve without coupling: +//! +//! | Crate | Owns | +//! |-------|------| +//! | **neuromod** | Canonical neuron models — LIF, Izhikevich, Hodgkin-Huxley, GIF, FitzHugh-Nagumo, Lapicque | +//! | **synaptic-mesh** | Topology, wiring, delay infrastructure, CSR sparse maps, [`router::ChannelRouter`], and its router-internal [`NeuromodNeuron`] integration primitive | //! //! ## Quick start //! @@ -90,7 +99,6 @@ pub mod delay; pub mod error; pub mod mesh; -pub mod neuromod; pub mod topology; pub mod types; @@ -109,10 +117,8 @@ pub use types::{ ConnectionModel, DelayModel, DelayTicks, NeuronId, Polarity, SynapseDescriptor, TopologyConfig, }; -pub use neuromod::NeuromodNeuron; - -// Generic router exports -pub use router::{ChannelRouter, NeuromodState, RouterConfig, RoutingDecision}; +// Generic router exports (NeuromodNeuron is a router-internal NIF primitive; see "Crate boundary" above) +pub use router::{ChannelRouter, NeuromodNeuron, NeuromodState, RouterConfig, RoutingDecision}; // Backward-compatible router exports (deprecated) pub use router::{AHL_NUM_CHANNELS, AhlRouter}; diff --git a/src/neuromod.rs b/src/neuromod.rs deleted file mode 100644 index 243e8ab..0000000 --- a/src/neuromod.rs +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -//! Neuromodulatory Integrative Fixed-threshold (NIF) neuron model. -//! -//! Provides a neuron model where the effective synaptic input is modulated -//! by a dynamic gain parameter, allowing for global or local modulation -//! of signal integration sensitivity. - -use serde::{Deserialize, Serialize}; - -/// A neuron model with neuromodulatory gain control. -/// -/// $V_{t+1} = V_t + (G \cdot I_{syn}) - \lambda(V_t - V_{rest})$ -/// where $G$ is the modulation gain and $\lambda$ is the leak rate. -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct NeuromodNeuron { - /// Current membrane potential. - pub v: f32, - /// Resting membrane potential. - pub v_rest: f32, - /// Reset potential after a spike. - pub v_reset: f32, - /// Passive leak rate per timestep. - pub leak: f32, - /// Firing threshold. - pub threshold: f32, - - /// Neuromodulatory gain (scales incoming stimulus). - pub gain: f32, - - /// Synaptic weights — one per input channel. - pub weights: Vec, - /// Whether the neuron fired in the last timestep. - pub last_spike: bool, -} - -impl Default for NeuromodNeuron { - fn default() -> Self { - Self { - v: 0.0, - v_rest: 0.0, - v_reset: 0.0, - leak: 0.12, - threshold: 0.25, - gain: 1.0, - weights: Vec::new(), - last_spike: false, - } - } -} - -impl NeuromodNeuron { - pub fn new() -> Self { - Self::default() - } - - /// Advance neuron dynamics by one timestep. - /// - /// The `stimulus` is scaled by the neuron's current `gain`. - pub fn integrate(&mut self, stimulus: f32) { - // Apply modulated integration - self.v += stimulus * self.gain; - // Apply leak towards resting potential - self.v -= (self.v - self.v_rest) * self.leak; - } - - /// Check if the neuron spikes. Resets V on fire. - pub fn check_fire(&mut self) -> Option { - if self.v >= self.threshold { - let peak = self.v; - self.v = self.v_reset; - self.last_spike = true; - return Some(peak); - } - self.last_spike = false; - None - } - - /// Update the modulation gain. - pub fn set_gain(&mut self, new_gain: f32) { - self.gain = new_gain; - } -} diff --git a/src/router.rs b/src/router.rs index f8c79ff..2c2b3e5 100644 --- a/src/router.rs +++ b/src/router.rs @@ -9,7 +9,6 @@ //! neuromodulatory routing — channels strengthen with use (dopamine-gated) //! and weaken when idle (use-it-or-lose-it plasticity). -use crate::neuromod::NeuromodNeuron; use serde::{Deserialize, Serialize}; /// Number of input channels for the default 3-channel router (backward compatible). @@ -21,6 +20,87 @@ const ROUTING_TIMESTEPS: usize = 16; /// Minimum firing rate (spikes / `ROUTING_TIMESTEPS`) to activate a channel. const MIN_FIRE_RATE: f32 = 0.1875; +/// Neuromodulatory Integrative Fixed-threshold (NIF) neuron. +/// +/// This is a **router-internal integration primitive** for [`ChannelRouter`], +/// not a general-purpose neuron model — canonical neuron models (LIF, +/// Izhikevich, Hodgkin-Huxley, GIF, FitzHugh-Nagumo, Lapicque) live in the +/// separate `neuromod` crate, which `synaptic-mesh` intentionally does not +/// depend on. See the crate-level docs for the full boundary rationale. +/// +/// $V_{t+1} = V_t + (G \cdot I_{syn}) - \lambda(V_t - V_{rest})$ +/// where $G$ is the modulation gain and $\lambda$ is the leak rate. +#[derive(Clone, Serialize, Deserialize, Debug)] +#[serde(default)] +pub struct NeuromodNeuron { + /// Current membrane potential. + pub v: f32, + /// Resting membrane potential. + pub v_rest: f32, + /// Reset potential after a spike. + pub v_reset: f32, + /// Passive leak rate per timestep. + pub leak: f32, + /// Firing threshold. + pub threshold: f32, + + /// Neuromodulatory gain (scales incoming stimulus). + pub gain: f32, + + /// Synaptic weights — one per input channel. + pub weights: Vec, + /// Whether the neuron fired in the last timestep. + pub last_spike: bool, +} + +impl Default for NeuromodNeuron { + fn default() -> Self { + Self { + v: 0.0, + v_rest: 0.0, + v_reset: 0.0, + leak: 0.12, + threshold: 0.25, + gain: 1.0, + weights: Vec::new(), + last_spike: false, + } + } +} + +impl NeuromodNeuron { + pub fn new() -> Self { + Self::default() + } + + /// Advance neuron dynamics by one timestep. + /// + /// The `stimulus` is scaled by the neuron's current `gain`. + pub fn integrate(&mut self, stimulus: f32) { + // Apply modulated integration + self.v += stimulus * self.gain; + // Apply leak towards resting potential + self.v -= (self.v - self.v_rest) * self.leak; + } + + /// Check if the neuron spikes. Resets V on fire. + pub fn check_fire(&mut self) -> Option { + if self.v >= self.threshold { + let peak = self.v; + self.v = self.v_reset; + self.last_spike = true; + return Some(peak); + } + self.last_spike = false; + None + } + + /// Update the modulation gain. + pub fn set_gain(&mut self, new_gain: f32) { + self.gain = new_gain; + } +} + /// Configuration for a generic channel router. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RouterConfig { diff --git a/src/tests.rs b/src/tests.rs index 1833cba..431d288 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -use crate::neuromod::NeuromodNeuron; -use crate::router::{ChannelRouter, NeuromodState, RouterConfig}; +use crate::router::{ChannelRouter, NeuromodNeuron, NeuromodState, RouterConfig}; #[test] fn channel_0_pulse_activates_channel_0() { @@ -243,6 +242,30 @@ fn deserialized_router_with_empty_inner_baseline_weights_recovers() { ); } +#[test] +fn deserialized_router_with_missing_neuron_fields_recovers() { + // Regression test: older or hand-authored payloads may omit fields from a + // NeuromodNeuron. Deserialization must use the neuron's defaults so the + // router's existing lazy state repair can run on the first route. + let router = ChannelRouter::new(); + let mut json: serde_json::Value = + serde_json::to_value(&router).expect("Fresh router must serialize"); + json["neurons"][0] + .as_object_mut() + .unwrap() + .remove("weights"); + + let mut router: ChannelRouter = serde_json::from_value(json) + .expect("Missing neuron fields should deserialize using defaults"); + + let result = router.route_modulated([0.5, 0.0, 0.0], &NeuromodState::balanced()); + assert!( + result.is_ok(), + "Router with missing neuron fields must self-heal on first route: {:?}", + result.err() + ); +} + #[test] fn apply_feedback_on_deserialized_router_with_malformed_baseline_does_not_panic() { // Regression test for chatgpt-codex P2 thread #NyuBl / devin-ai BUG #NytR0.