From efc8d888f4e50e589f292e669c6e3ed54d3f5c78 Mon Sep 17 00:00:00 2001 From: Grant Parry Date: Sat, 21 Feb 2026 18:55:42 -0700 Subject: [PATCH 1/7] docs(adr): ADR-045 Signal Extensions Plugin Architecture Proposes a generic extension mechanism for QualitySignal so consumers can attach domain-specific structured data without upstream changes. Co-Authored-By: Claude Opus 4.6 --- ...5-signal-extensions-plugin-architecture.md | 298 ++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 docs/adr/ADR-045-signal-extensions-plugin-architecture.md diff --git a/docs/adr/ADR-045-signal-extensions-plugin-architecture.md b/docs/adr/ADR-045-signal-extensions-plugin-architecture.md new file mode 100644 index 000000000..4a3a21aaa --- /dev/null +++ b/docs/adr/ADR-045-signal-extensions-plugin-architecture.md @@ -0,0 +1,298 @@ +# ADR-045: Signal Extensions Plugin Architecture + +| Field | Value | +|-------------|------------------------------------------------| +| Status | Proposed | +| Date | 2026-02-21 | +| Authors | @grparry (proposal) | +| Supersedes | — | +| Extends | ADR-043 (External Intelligence Providers) | + +## Context + +ADR-043 established the `IntelligenceProvider` trait and `QualitySignal` type for feeding external quality data into ruvLLM. The current `QualitySignal` carries a composite score, an outcome, and 9 optional quality factors. This covers the common case well. + +However, different consumers of ruvLLM operate in very different environments — workflow engines, CI/CD pipelines, coding assistants, multi-agent frameworks, IDE plugins — and each has domain-specific context that could improve learning quality if it could be attached to signals. Today, that context is lost because `QualitySignal` has a fixed schema with no extension point. + +### Examples of Consumer-Specific Context + +These are illustrative, not prescriptive — the point is that each consumer has *different* structured data: + +- **Workflow engines** may want to attach execution step sequences, timing data, or parent-child execution relationships. +- **CI/CD pipelines** may want to attach pipeline stage, test matrix configuration, or build environment details. +- **Coding assistants** may want to attach file paths modified, AST diff summaries, or language-specific metrics. +- **Multi-agent systems** may want to attach agent role assignments, inter-agent message counts, or coordination overhead. + +No single set of fields can serve all of these. Adding consumer-specific fields to `QualitySignal` would create a maintenance burden upstream and couple ruvLLM to external systems' data models. + +### Design Principle + +Rather than prescribing what structured data looks like, give consumers a generic, typed extension mechanism. RuvLLM provides the hooks; consumers define the content. + +## Decision + +**Add an optional `extensions` field to `QualitySignal` and a `SignalExtensionHandler` trait for consumers that want ruvLLM to act on their extension data.** + +This is a two-layer design: + +1. **Transport layer** — `QualitySignal.extensions` carries arbitrary typed JSON, keyed by namespace. No ruvLLM code changes needed to add new extension types. Any provider can attach any data. + +2. **Processing layer** — consumers that want ruvLLM to *use* their extension data (e.g., for richer SONA trajectories or HNSW features) register a `SignalExtensionHandler`. This is optional — unhandled extensions are preserved but not processed. + +### Changes to QualitySignal + +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QualitySignal { + // ── Existing fields (unchanged) ────────────────────────────── + pub id: String, + pub task_description: String, + pub outcome: Outcome, + pub quality_score: f32, + #[serde(default)] + pub human_verdict: Option, + #[serde(default)] + pub quality_factors: Option, + pub completed_at: String, + + // ── New: generic extension data ────────────────────────────── + /// Provider-specific structured data, keyed by namespace. + /// + /// Namespaces prevent collisions between providers. Convention: + /// use your provider name or organization as the namespace key + /// (e.g., "my-pipeline", "ci-system", "code-assistant"). + /// + /// RuvLLM preserves extension data through the signal pipeline. + /// To have ruvLLM act on extension data (extract SONA features, + /// generate embeddings, etc.), register a `SignalExtensionHandler`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub extensions: Option>, +} +``` + +### SignalExtensionHandler Trait + +```rust +/// Handler for processing provider-specific extension data on signals. +/// +/// Consumers register handlers with `IntelligenceLoader` to extract +/// features from their extension data. Handlers are called during +/// signal ingestion, after the core signal fields have been processed. +/// +/// This is optional — signals with unhandled extensions are still +/// ingested normally using their core fields. +pub trait SignalExtensionHandler: Send + Sync { + /// The namespace this handler processes (must match extension key). + fn namespace(&self) -> &str; + + /// Extract additional SONA trajectory features from extension data. + /// + /// Returns key-value pairs that are merged into the trajectory's + /// metadata. Called once per signal during ingestion. + fn extract_trajectory_features( + &self, + extension_data: &serde_json::Value, + signal: &QualitySignal, + ) -> Result> { + let _ = (extension_data, signal); + Ok(HashMap::new()) + } + + /// Extract additional embedding features from extension data. + /// + /// Returns text fragments that are appended to the task description + /// before embedding generation. This allows extension data to + /// influence HNSW clustering without changing the embedding model. + fn extract_embedding_context( + &self, + extension_data: &serde_json::Value, + signal: &QualitySignal, + ) -> Result> { + let _ = (extension_data, signal); + Ok(vec![]) + } + + /// Extract additional router calibration features. + /// + /// Returns key-value pairs used as features when the model router + /// estimates task complexity. Called during calibration updates. + fn extract_router_features( + &self, + extension_data: &serde_json::Value, + signal: &QualitySignal, + ) -> Result> { + let _ = (extension_data, signal); + Ok(HashMap::new()) + } +} +``` + +### Changes to IntelligenceLoader + +```rust +impl IntelligenceLoader { + // Existing methods unchanged + + /// Register a handler for processing extension data in a specific namespace. + pub fn register_extension_handler( + &mut self, + handler: Box, + ) { + self.extension_handlers.insert( + handler.namespace().to_string(), + handler, + ); + } +} +``` + +During `load_all_signals()`, after processing core signal fields, the loader checks each signal's `extensions` map. For each namespace key that has a registered handler, it calls the handler's extraction methods and merges the results into the trajectory/embedding/router data. + +## Architecture + +``` +Provider writes signal Consumer registers handler + │ │ + ▼ ▼ +┌──────────────┐ ┌─────────────────────────┐ +│ QualitySignal │ │ SignalExtensionHandler │ +│ .extensions │──namespace──▶ │ .namespace() │ +│ {"my-ns": │ match │ .extract_trajectory_* │ +│ {...}} │ │ .extract_embedding_* │ +└──────────────┘ │ .extract_router_* │ + │ └─────────────────────────┘ + ▼ │ +┌──────────────┐ ▼ +│ Intelligence │ ┌───────────────────────┐ +│ Loader │─── calls ──▶│ Merge extracted features│ +│ │ │ into SONA / HNSW / │ +│ │ │ router pipelines │ +└──────────────┘ └───────────────────────┘ +``` + +**No handler registered?** The signal is ingested normally using its core fields. The extension data is preserved in the signal record but not acted upon. Zero overhead. + +## Design Constraints + +- **Backward compatible.** `extensions` is `Option` with `#[serde(default)]`. Existing signals deserialize without error. Existing providers compile and run without changes. +- **No prescribed schema.** ruvLLM does not define what goes in extension data. Each consumer defines their own namespace and schema. +- **Namespace isolation.** Multiple providers can attach extension data to the same signal without collision. Each handler only processes its own namespace. +- **All handler methods have defaults.** Consumers implement only the extraction methods relevant to their data. A handler that only cares about SONA trajectories can skip embedding and router methods. +- **Payload size is consumer-controlled.** ruvLLM does not validate extension data size. Consumers are responsible for keeping their extensions reasonable. The file-based provider's existing 10 MiB limit provides a natural ceiling. + +## Existing Code References + +| Item | Status | Location | +|------|--------|----------| +| `QualitySignal` struct | EXISTS | `crates/ruvllm/src/intelligence/mod.rs` | +| `QualityFactors` struct | EXISTS | `crates/ruvllm/src/intelligence/mod.rs` | +| `IntelligenceProvider` trait | EXISTS | `crates/ruvllm/src/intelligence/mod.rs` | +| `FileSignalProvider` | EXISTS | `crates/ruvllm/src/intelligence/mod.rs` | +| `IntelligenceLoader` | EXISTS | `crates/ruvllm/src/intelligence/mod.rs` | +| `SignalExtensionHandler` trait | NEW | `crates/ruvllm/src/intelligence/mod.rs` | + +## Implementation + +### Files Modified + +| # | Path | Changes | +|---|------|---------| +| 1 | `crates/ruvllm/src/intelligence/mod.rs` | Add `extensions` field to `QualitySignal`. Add `SignalExtensionHandler` trait. Add `register_extension_handler()` to `IntelligenceLoader`. Add handler dispatch in signal ingestion. | +| 2 | `npm/packages/ruvllm/src/intelligence.ts` | Add `extensions?: Record` to `QualitySignal` interface. Add `SignalExtensionHandler` interface. | + +### Files Created + +| # | Path | Description | +|---|------|-------------| +| 1 | `docs/adr/ADR-045-structured-quality-signals.md` | This ADR | + +## Example: Consumer Implementation + +A workflow engine that wants ruvLLM to learn from execution step sequences would: + +1. **Populate extensions when emitting signals:** + +```json +{ + "id": "task-123", + "task_description": "Refactor authentication module", + "outcome": "success", + "quality_score": 0.87, + "extensions": { + "my-workflow-engine": { + "steps": [ + { "name": "plan", "duration_ms": 2300 }, + { "name": "implement", "duration_ms": 15000 }, + { "name": "review", "duration_ms": 4200 } + ], + "total_nodes": 5, + "parent_execution": "exec-456" + } + } +} +``` + +2. **Register a handler to extract features:** + +```rust +struct MyWorkflowHandler; + +impl SignalExtensionHandler for MyWorkflowHandler { + fn namespace(&self) -> &str { "my-workflow-engine" } + + fn extract_trajectory_features( + &self, + data: &serde_json::Value, + _signal: &QualitySignal, + ) -> Result> { + let mut features = HashMap::new(); + if let Some(steps) = data.get("steps") { + features.insert("step_count".into(), json!(steps.as_array().map(|a| a.len()).unwrap_or(0))); + } + if let Some(parent) = data.get("parent_execution") { + features.insert("parent_execution".into(), parent.clone()); + } + Ok(features) + } + + fn extract_router_features( + &self, + data: &serde_json::Value, + _signal: &QualitySignal, + ) -> Result> { + let mut features = HashMap::new(); + if let Some(n) = data.get("total_nodes").and_then(|v| v.as_f64()) { + features.insert("pipeline_complexity".into(), n as f32); + } + Ok(features) + } +} + +// Registration: +loader.register_extension_handler(Box::new(MyWorkflowHandler)); +``` + +A CI/CD system would do the same with its own namespace and its own data shape — without any ruvLLM changes. + +## Consequences + +### Positive + +1. **Consumers own their data model.** No upstream PRs needed to add new structured data. Define a namespace, populate it, optionally register a handler. +2. **ruvLLM stays generic.** The core signal type doesn't accumulate domain-specific fields from every consumer. +3. **Incremental adoption.** Start by attaching extension data (preserved but not processed). Add a handler later when you want ruvLLM to act on it. +4. **Fully backward compatible.** Existing signals, providers, and consumers work unchanged. +5. **Composable.** Multiple consumers can attach extensions to the same signal. Multiple handlers can be registered. They don't interfere with each other. + +### Negative + +1. **No schema validation.** Extension data is `serde_json::Value` — ruvLLM cannot validate its structure. Schema enforcement is the handler's responsibility. +2. **Handler registration requires Rust.** Non-Rust consumers can attach extension data via JSON files, but processing that data requires a Rust handler. (Mitigation: a future ADR could add a declarative handler config for common patterns like "extract field X as router feature Y".) +3. **Testing surface.** Each handler needs its own tests. ruvLLM's test suite covers the dispatch mechanism; consumers test their handlers. + +## Related Decisions + +- **ADR-002**: RuvLLM Integration with Ruvector — Witness Log schema with `quality_score: f32` +- **ADR-043**: External Intelligence Providers — established `IntelligenceProvider` trait and `QualitySignal` type (this ADR extends the signal type with a generic extension point) +- **ADR-CE-021**: Shared SONA — multiple external systems contributing trajectories +- **ADR-004**: KV Cache Management — tiered approach benefiting from richer calibration signals From 35c72f01c892945060a7574003460413f039140a Mon Sep 17 00:00:00 2001 From: Grant Parry Date: Tue, 24 Feb 2026 14:55:55 -0700 Subject: [PATCH 2/7] Implement signal extensions plugin architecture (ADR-045) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add generic extension mechanism for QualitySignal, allowing consumers to attach and process provider-specific structured data without upstream schema changes. Rust changes: - Add `extensions: Option>` to QualitySignal - Add `SignalExtensionHandler` trait with extract_trajectory_features, extract_embedding_context, and extract_router_features methods - Add `register_extension_handler()` and `process_extensions()` to IntelligenceLoader - Add `SignalExtensionResults` struct for handler output - Add 7 tests covering serde roundtrip, backward compatibility, handler dispatch, namespace isolation, and file provider passthrough TypeScript changes: - Add `extensions?: Record` to QualitySignal interface - Add `SignalExtensionHandler` and `SignalExtensionResults` interfaces - Add `registerExtensionHandler()` and `processExtensions()` to IntelligenceLoader class - Update FileSignalProvider to pass through extension data All changes are backward compatible — existing signals, providers, and consumers work unchanged. Extensions default to None/undefined when absent. Co-Authored-By: Claude Opus 4.6 --- crates/ruvllm/src/intelligence/mod.rs | 366 ++++++++++++++++++++++++ crates/ruvllm/src/lib.rs | 1 + npm/packages/ruvllm/src/intelligence.ts | 120 ++++++++ 3 files changed, 487 insertions(+) diff --git a/crates/ruvllm/src/intelligence/mod.rs b/crates/ruvllm/src/intelligence/mod.rs index bac38f899..ba8536432 100644 --- a/crates/ruvllm/src/intelligence/mod.rs +++ b/crates/ruvllm/src/intelligence/mod.rs @@ -42,6 +42,7 @@ use crate::error::Result; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::path::{Path, PathBuf}; // --------------------------------------------------------------------------- @@ -105,6 +106,18 @@ pub struct QualitySignal { /// ISO 8601 timestamp of task completion pub completed_at: String, + + /// Provider-specific structured data, keyed by namespace. + /// + /// Namespaces prevent collisions between providers. Convention: + /// use your provider name or organization as the namespace key + /// (e.g., `"my-pipeline"`, `"ci-system"`). + /// + /// RuvLLM preserves extension data through the signal pipeline. + /// To have ruvLLM act on extension data (extract SONA features, + /// generate embeddings, etc.), register a [`SignalExtensionHandler`]. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub extensions: Option>, } /// Granular quality factor breakdown. @@ -217,6 +230,89 @@ pub trait IntelligenceProvider: Send + Sync { } } +// --------------------------------------------------------------------------- +// SignalExtensionHandler — plugin trait for processing extension data +// --------------------------------------------------------------------------- + +/// Handler for processing provider-specific extension data on signals. +/// +/// Consumers register handlers with [`IntelligenceLoader`] to extract +/// features from their extension data. Handlers are called during +/// signal ingestion, after the core signal fields have been processed. +/// +/// This is optional — signals with unhandled extensions are still +/// ingested normally using their core fields. +/// +/// # Examples +/// +/// ```rust,ignore +/// use ruvllm::intelligence::{SignalExtensionHandler, QualitySignal}; +/// use ruvllm::error::Result; +/// use std::collections::HashMap; +/// +/// struct MyWorkflowHandler; +/// +/// impl SignalExtensionHandler for MyWorkflowHandler { +/// fn namespace(&self) -> &str { "my-workflow-engine" } +/// +/// fn extract_trajectory_features( +/// &self, +/// data: &serde_json::Value, +/// _signal: &QualitySignal, +/// ) -> Result> { +/// let mut features = HashMap::new(); +/// if let Some(steps) = data.get("steps").and_then(|v| v.as_array()) { +/// features.insert("step_count".into(), serde_json::json!(steps.len())); +/// } +/// Ok(features) +/// } +/// } +/// ``` +pub trait SignalExtensionHandler: Send + Sync { + /// The namespace this handler processes (must match an extension key). + fn namespace(&self) -> &str; + + /// Extract additional SONA trajectory features from extension data. + /// + /// Returns key-value pairs that are merged into the trajectory's + /// metadata. Called once per signal during ingestion. + fn extract_trajectory_features( + &self, + extension_data: &serde_json::Value, + signal: &QualitySignal, + ) -> Result> { + let _ = (extension_data, signal); + Ok(HashMap::new()) + } + + /// Extract additional embedding features from extension data. + /// + /// Returns text fragments that are appended to the task description + /// before embedding generation. This allows extension data to + /// influence HNSW clustering without changing the embedding model. + fn extract_embedding_context( + &self, + extension_data: &serde_json::Value, + signal: &QualitySignal, + ) -> Result> { + let _ = (extension_data, signal); + Ok(vec![]) + } + + /// Extract additional router calibration features. + /// + /// Returns key-value pairs used as features when the model router + /// estimates task complexity. Called during calibration updates. + fn extract_router_features( + &self, + extension_data: &serde_json::Value, + signal: &QualitySignal, + ) -> Result> { + let _ = (extension_data, signal); + Ok(HashMap::new()) + } +} + // --------------------------------------------------------------------------- // FileSignalProvider — built-in file-based provider // --------------------------------------------------------------------------- @@ -349,6 +445,7 @@ impl IntelligenceProvider for FileSignalProvider { /// with no allocations beyond the empty `Vec`. pub struct IntelligenceLoader { providers: Vec>, + extension_handlers: HashMap>, } impl IntelligenceLoader { @@ -356,6 +453,7 @@ impl IntelligenceLoader { pub fn new() -> Self { Self { providers: Vec::new(), + extension_handlers: HashMap::new(), } } @@ -376,6 +474,63 @@ impl IntelligenceLoader { self.providers.iter().map(|p| p.name()).collect() } + /// Register a handler for processing extension data in a specific namespace. + /// + /// Only one handler per namespace is allowed. Registering a new handler + /// for an existing namespace replaces the previous handler. + pub fn register_extension_handler(&mut self, handler: Box) { + self.extension_handlers + .insert(handler.namespace().to_string(), handler); + } + + /// Returns the number of registered extension handlers. + pub fn extension_handler_count(&self) -> usize { + self.extension_handlers.len() + } + + /// Returns the namespaces of all registered extension handlers. + pub fn extension_handler_namespaces(&self) -> Vec<&str> { + self.extension_handlers.keys().map(|k| k.as_str()).collect() + } + + /// Process a signal's extension data through registered handlers. + /// + /// For each namespace in the signal's `extensions` that has a registered + /// handler, calls the handler's extraction methods. Returns a + /// [`SignalExtensionResults`] with all extracted features. + /// + /// Namespaces without a registered handler are silently skipped — + /// the extension data is preserved on the signal but not processed. + pub fn process_extensions(&self, signal: &QualitySignal) -> SignalExtensionResults { + let mut results = SignalExtensionResults::default(); + + let extensions = match &signal.extensions { + Some(ext) => ext, + None => return results, + }; + + for (namespace, data) in extensions { + let handler = match self.extension_handlers.get(namespace) { + Some(h) => h, + None => continue, + }; + + if let Ok(features) = handler.extract_trajectory_features(data, signal) { + results.trajectory_features.extend(features); + } + + if let Ok(context) = handler.extract_embedding_context(data, signal) { + results.embedding_context.extend(context); + } + + if let Ok(features) = handler.extract_router_features(data, signal) { + results.router_features.extend(features); + } + } + + results + } + /// Load signals from all registered providers. /// /// Signals from each provider are collected into a flat list. @@ -430,6 +585,17 @@ impl Default for IntelligenceLoader { } } +/// Results from processing a signal's extension data through handlers. +#[derive(Debug, Clone, Default)] +pub struct SignalExtensionResults { + /// Additional SONA trajectory features extracted from extensions. + pub trajectory_features: HashMap, + /// Additional text fragments for embedding generation. + pub embedding_context: Vec, + /// Additional router calibration features. + pub router_features: HashMap, +} + /// Error from a single provider during batch loading. #[derive(Debug, Clone)] pub struct ProviderError { @@ -506,6 +672,7 @@ mod tests { human_verdict: None, quality_factors: None, completed_at: "2025-02-21T00:00:00Z".to_string(), + extensions: None, } } @@ -654,6 +821,7 @@ mod tests { ..Default::default() }), completed_at: "2025-02-21T12:00:00Z".to_string(), + extensions: None, }; let json = serde_json::to_string(&signal).unwrap(); @@ -664,4 +832,202 @@ mod tests { let factors = parsed.quality_factors.unwrap(); assert!((factors.tests_passing.unwrap() - 1.0).abs() < f32::EPSILON); } + + // -- Extension tests --------------------------------------------------- + + #[test] + fn extensions_serde_roundtrip() { + let mut ext = HashMap::new(); + ext.insert( + "my-pipeline".to_string(), + serde_json::json!({ + "steps": [{"name": "plan", "duration_ms": 2300}], + "total_nodes": 5 + }), + ); + + let signal = QualitySignal { + id: "ext1".to_string(), + task_description: "Extension roundtrip".to_string(), + outcome: Outcome::Success, + quality_score: 0.9, + human_verdict: None, + quality_factors: None, + completed_at: "2025-02-21T00:00:00Z".to_string(), + extensions: Some(ext), + }; + + let json = serde_json::to_string(&signal).unwrap(); + let parsed: QualitySignal = serde_json::from_str(&json).unwrap(); + assert!(parsed.extensions.is_some()); + let exts = parsed.extensions.unwrap(); + assert!(exts.contains_key("my-pipeline")); + assert_eq!(exts["my-pipeline"]["total_nodes"], 5); + } + + #[test] + fn extensions_backward_compatible_deserialize() { + // JSON without extensions field — should deserialize fine + let json = r#"{ + "id": "bc1", + "task_description": "No extensions", + "outcome": "success", + "quality_score": 0.8, + "completed_at": "2025-02-21T00:00:00Z" + }"#; + let signal: QualitySignal = serde_json::from_str(json).unwrap(); + assert!(signal.extensions.is_none()); + } + + #[test] + fn extensions_none_not_serialized() { + let signal = make_signal("skip1", 0.5); + let json = serde_json::to_string(&signal).unwrap(); + assert!(!json.contains("extensions")); + } + + /// Mock handler for testing extension processing + struct MockExtensionHandler; + + impl SignalExtensionHandler for MockExtensionHandler { + fn namespace(&self) -> &str { + "test-ns" + } + + fn extract_trajectory_features( + &self, + data: &serde_json::Value, + _signal: &QualitySignal, + ) -> Result> { + let mut features = HashMap::new(); + if let Some(count) = data.get("step_count") { + features.insert("step_count".into(), count.clone()); + } + Ok(features) + } + + fn extract_embedding_context( + &self, + data: &serde_json::Value, + _signal: &QualitySignal, + ) -> Result> { + let mut ctx = vec![]; + if let Some(tag) = data.get("tag").and_then(|v| v.as_str()) { + ctx.push(tag.to_string()); + } + Ok(ctx) + } + + fn extract_router_features( + &self, + data: &serde_json::Value, + _signal: &QualitySignal, + ) -> Result> { + let mut features = HashMap::new(); + if let Some(c) = data.get("complexity").and_then(|v| v.as_f64()) { + features.insert("pipeline_complexity".into(), c as f32); + } + Ok(features) + } + } + + #[test] + fn extension_handler_registration() { + let mut loader = IntelligenceLoader::new(); + assert_eq!(loader.extension_handler_count(), 0); + + loader.register_extension_handler(Box::new(MockExtensionHandler)); + assert_eq!(loader.extension_handler_count(), 1); + assert!(loader + .extension_handler_namespaces() + .contains(&"test-ns")); + } + + #[test] + fn extension_handler_processes_matching_namespace() { + let mut loader = IntelligenceLoader::new(); + loader.register_extension_handler(Box::new(MockExtensionHandler)); + + let mut ext = HashMap::new(); + ext.insert( + "test-ns".to_string(), + serde_json::json!({ + "step_count": 3, + "tag": "refactor", + "complexity": 0.75 + }), + ); + + let signal = QualitySignal { + extensions: Some(ext), + ..make_signal("eh1", 0.9) + }; + + let results = loader.process_extensions(&signal); + assert_eq!(results.trajectory_features["step_count"], 3); + assert_eq!(results.embedding_context, vec!["refactor"]); + assert!((results.router_features["pipeline_complexity"] - 0.75).abs() < f32::EPSILON); + } + + #[test] + fn extension_handler_skips_unmatched_namespace() { + let mut loader = IntelligenceLoader::new(); + loader.register_extension_handler(Box::new(MockExtensionHandler)); + + let mut ext = HashMap::new(); + ext.insert( + "other-ns".to_string(), + serde_json::json!({"step_count": 5}), + ); + + let signal = QualitySignal { + extensions: Some(ext), + ..make_signal("eh2", 0.8) + }; + + let results = loader.process_extensions(&signal); + assert!(results.trajectory_features.is_empty()); + assert!(results.embedding_context.is_empty()); + assert!(results.router_features.is_empty()); + } + + #[test] + fn extension_handler_no_extensions_returns_empty() { + let mut loader = IntelligenceLoader::new(); + loader.register_extension_handler(Box::new(MockExtensionHandler)); + + let signal = make_signal("eh3", 0.7); + let results = loader.process_extensions(&signal); + assert!(results.trajectory_features.is_empty()); + } + + #[test] + fn file_provider_reads_extensions() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("ext-signals.json"); + let mut f = std::fs::File::create(&path).unwrap(); + write!( + f, + r#"[ + {{ + "id": "fe1", + "task_description": "With extensions", + "outcome": "success", + "quality_score": 0.85, + "completed_at": "2025-02-21T10:00:00Z", + "extensions": {{ + "ci-system": {{"pipeline": "main", "stage": "deploy"}} + }} + }} + ]"# + ) + .unwrap(); + + let provider = FileSignalProvider::new(path); + let signals = provider.load_signals().unwrap(); + assert_eq!(signals.len(), 1); + assert!(signals[0].extensions.is_some()); + let exts = signals[0].extensions.as_ref().unwrap(); + assert_eq!(exts["ci-system"]["pipeline"], "main"); + } } diff --git a/crates/ruvllm/src/lib.rs b/crates/ruvllm/src/lib.rs index f991a3145..5360f44ed 100644 --- a/crates/ruvllm/src/lib.rs +++ b/crates/ruvllm/src/lib.rs @@ -491,6 +491,7 @@ pub use ruvector_integration::{ pub use intelligence::{ FileSignalProvider, HumanVerdict, IntelligenceLoader, IntelligenceProvider, Outcome, ProviderError, ProviderQualityWeights, ProviderResult, QualityFactors, QualitySignal, + SignalExtensionHandler, SignalExtensionResults, }; // Quality scoring exports diff --git a/npm/packages/ruvllm/src/intelligence.ts b/npm/packages/ruvllm/src/intelligence.ts index e9250ef5f..1f478b70f 100644 --- a/npm/packages/ruvllm/src/intelligence.ts +++ b/npm/packages/ruvllm/src/intelligence.ts @@ -53,6 +53,16 @@ export interface QualitySignal { qualityFactors?: QualityFactors; /** ISO 8601 timestamp of task completion */ completedAt: string; + /** + * Provider-specific structured data, keyed by namespace. + * + * Namespaces prevent collisions between providers. Convention: + * use your provider name or organization as the namespace key. + * + * RuvLLM preserves extension data through the signal pipeline. + * To have ruvLLM act on extension data, register a SignalExtensionHandler. + */ + extensions?: Record; } /** @@ -115,6 +125,57 @@ export interface IntelligenceProvider { qualityWeights?(): ProviderQualityWeights | undefined; } +/** + * Handler for processing provider-specific extension data on signals. + * + * Register handlers with IntelligenceLoader to extract features from + * extension data. Handlers are called during signal processing, after + * core signal fields have been processed. + * + * This is optional — signals with unhandled extensions are still + * processed normally using their core fields. + */ +export interface SignalExtensionHandler { + /** The namespace this handler processes (must match an extension key) */ + namespace(): string; + + /** + * Extract additional SONA trajectory features from extension data. + * Returns key-value pairs merged into the trajectory's metadata. + */ + extractTrajectoryFeatures?( + extensionData: unknown, + signal: QualitySignal, + ): Record; + + /** + * Extract additional embedding features from extension data. + * Returns text fragments appended to the task description before embedding. + */ + extractEmbeddingContext?( + extensionData: unknown, + signal: QualitySignal, + ): string[]; + + /** + * Extract additional router calibration features. + * Returns key-value pairs used as features for complexity estimation. + */ + extractRouterFeatures?( + extensionData: unknown, + signal: QualitySignal, + ): Record; +} + +/** + * Results from processing a signal's extension data through handlers. + */ +export interface SignalExtensionResults { + trajectoryFeatures: Record; + embeddingContext: string[]; + routerFeatures: Record; +} + function asOptionalNumber(val: unknown): number | undefined { if (val === undefined || val === null) return undefined; const n = Number(val); @@ -197,6 +258,7 @@ export class FileSignalProvider implements IntelligenceProvider { return data.map((item: Record) => { const qfRaw = (item.quality_factors ?? item.qualityFactors) as Record | undefined; + const extRaw = item.extensions as Record | undefined; return { id: String(item.id ?? ''), taskDescription: String(item.task_description ?? item.taskDescription ?? ''), @@ -205,6 +267,7 @@ export class FileSignalProvider implements IntelligenceProvider { humanVerdict: validateVerdict(item.human_verdict ?? item.humanVerdict), qualityFactors: qfRaw ? mapQualityFactors(qfRaw) : undefined, completedAt: String(item.completed_at ?? item.completedAt ?? new Date().toISOString()), + extensions: extRaw && typeof extRaw === 'object' ? extRaw : undefined, }; }); } @@ -234,22 +297,79 @@ export class FileSignalProvider implements IntelligenceProvider { */ export class IntelligenceLoader { private providers: IntelligenceProvider[] = []; + private extensionHandlers: Map = new Map(); /** Register an external intelligence provider */ registerProvider(provider: IntelligenceProvider): void { this.providers.push(provider); } + /** + * Register a handler for processing extension data in a specific namespace. + * Only one handler per namespace is allowed; re-registering replaces the previous. + */ + registerExtensionHandler(handler: SignalExtensionHandler): void { + this.extensionHandlers.set(handler.namespace(), handler); + } + /** Returns the number of registered providers */ get providerCount(): number { return this.providers.length; } + /** Returns the number of registered extension handlers */ + get extensionHandlerCount(): number { + return this.extensionHandlers.size; + } + /** Returns the names of all registered providers */ get providerNames(): string[] { return this.providers.map(p => p.name()); } + /** Returns the namespaces of all registered extension handlers */ + get extensionHandlerNamespaces(): string[] { + return Array.from(this.extensionHandlers.keys()); + } + + /** + * Process a signal's extension data through registered handlers. + * + * For each namespace in the signal's extensions that has a registered + * handler, calls the handler's extraction methods. Namespaces without + * a registered handler are silently skipped. + */ + processExtensions(signal: QualitySignal): SignalExtensionResults { + const results: SignalExtensionResults = { + trajectoryFeatures: {}, + embeddingContext: [], + routerFeatures: {}, + }; + + if (!signal.extensions) return results; + + for (const [namespace, data] of Object.entries(signal.extensions)) { + const handler = this.extensionHandlers.get(namespace); + if (!handler) continue; + + try { + if (handler.extractTrajectoryFeatures) { + Object.assign(results.trajectoryFeatures, handler.extractTrajectoryFeatures(data, signal)); + } + if (handler.extractEmbeddingContext) { + results.embeddingContext.push(...handler.extractEmbeddingContext(data, signal)); + } + if (handler.extractRouterFeatures) { + Object.assign(results.routerFeatures, handler.extractRouterFeatures(data, signal)); + } + } catch { + // Non-fatal: handler errors don't block signal processing + } + } + + return results; + } + /** * Load signals from all registered providers. * From 351e6e033cd0bfd6b971b2b69eaacdf08bc892f6 Mon Sep 17 00:00:00 2001 From: Grant Parry Date: Tue, 24 Feb 2026 15:08:08 -0700 Subject: [PATCH 3/7] Add AcornflowExtensionHandler as reference implementation Demonstrates the SignalExtensionHandler trait (ADR-045) with a concrete handler for the "acornflow" namespace. Extracts reasoning chain stats, workflow names, semantic ref counts, and quality aggregates into trajectory features, embedding context, and router calibration features. Includes 4 tests: trajectory extraction, embedding context, router features, and end-to-end through IntelligenceLoader. Co-Authored-By: Claude Opus 4.6 --- .../src/intelligence/acornflow_handler.rs | 249 ++++++++++++++++++ crates/ruvllm/src/intelligence/mod.rs | 2 + crates/ruvllm/src/lib.rs | 7 +- 3 files changed, 255 insertions(+), 3 deletions(-) create mode 100644 crates/ruvllm/src/intelligence/acornflow_handler.rs diff --git a/crates/ruvllm/src/intelligence/acornflow_handler.rs b/crates/ruvllm/src/intelligence/acornflow_handler.rs new file mode 100644 index 000000000..c30f8225c --- /dev/null +++ b/crates/ruvllm/src/intelligence/acornflow_handler.rs @@ -0,0 +1,249 @@ +//! Acornflow extension handler for ruvLLM signal processing. +//! +//! Demonstrates the ADR-045 `SignalExtensionHandler` trait by extracting +//! features from the `"acornflow"` extension namespace that the +//! execution-analysis workflow attaches to signals. +//! +//! ## Extension Data Shape +//! +//! ```json +//! { +//! "acornflow": { +//! "source_execution_id": "exec-123", +//! "workflow_name": "deploy-service", +//! "reasoning_chain": [ +//! { "nodeName": "plan", "status": "completed", "durationMs": 2300 }, +//! { "nodeName": "implement", "status": "completed", "durationMs": 15000 } +//! ], +//! "chain_length": 2, +//! "semantic_refs": { "files": [...] }, +//! "quality_aggregate": { "mean": 0.85, "min": 0.7, "max": 1.0 } +//! } +//! } +//! ``` + +use crate::error::Result; +use crate::intelligence::{QualitySignal, SignalExtensionHandler}; +use std::collections::HashMap; + +/// Extension handler for signals produced by Acornflow's execution-analysis workflow. +/// +/// Extracts trajectory features (reasoning chain stats, semantic refs), +/// embedding context (workflow name, chain summary), and router features +/// (chain length, quality aggregate) from the `"acornflow"` namespace. +pub struct AcornflowExtensionHandler; + +impl SignalExtensionHandler for AcornflowExtensionHandler { + fn namespace(&self) -> &str { + "acornflow" + } + + fn extract_trajectory_features( + &self, + data: &serde_json::Value, + _signal: &QualitySignal, + ) -> Result> { + let mut features = HashMap::new(); + + if let Some(exec_id) = data.get("source_execution_id") { + features.insert("source_execution_id".into(), exec_id.clone()); + } + + if let Some(wf_name) = data.get("workflow_name") { + features.insert("workflow_name".into(), wf_name.clone()); + } + + if let Some(chain) = data.get("reasoning_chain").and_then(|v| v.as_array()) { + features.insert("chain_length".into(), serde_json::json!(chain.len())); + + // Extract step names for pattern matching + let step_names: Vec<&str> = chain + .iter() + .filter_map(|step| step.get("nodeName").and_then(|n| n.as_str())) + .collect(); + features.insert( + "chain_steps".into(), + serde_json::json!(step_names), + ); + + // Total duration across all steps + let total_ms: f64 = chain + .iter() + .filter_map(|step| step.get("durationMs").and_then(|d| d.as_f64())) + .sum(); + features.insert("total_duration_ms".into(), serde_json::json!(total_ms)); + } + + if let Some(refs) = data.get("semantic_refs") { + if let Some(files) = refs.get("files").and_then(|f| f.as_array()) { + features.insert("semantic_file_count".into(), serde_json::json!(files.len())); + } + } + + Ok(features) + } + + fn extract_embedding_context( + &self, + data: &serde_json::Value, + _signal: &QualitySignal, + ) -> Result> { + let mut context = vec![]; + + // Add workflow name for clustering signals by workflow type + if let Some(name) = data.get("workflow_name").and_then(|v| v.as_str()) { + context.push(format!("workflow:{}", name)); + } + + // Add chain step summary for semantic similarity + if let Some(chain) = data.get("reasoning_chain").and_then(|v| v.as_array()) { + let steps: Vec<&str> = chain + .iter() + .filter_map(|step| step.get("nodeName").and_then(|n| n.as_str())) + .collect(); + if !steps.is_empty() { + context.push(format!("steps:{}", steps.join("->"))); + } + } + + Ok(context) + } + + fn extract_router_features( + &self, + data: &serde_json::Value, + _signal: &QualitySignal, + ) -> Result> { + let mut features = HashMap::new(); + + if let Some(len) = data.get("chain_length").and_then(|v| v.as_f64()) { + features.insert("chain_length".into(), len as f32); + } + + if let Some(agg) = data.get("quality_aggregate") { + if let Some(mean) = agg.get("mean").and_then(|v| v.as_f64()) { + features.insert("quality_mean".into(), mean as f32); + } + if let Some(min) = agg.get("min").and_then(|v| v.as_f64()) { + features.insert("quality_min".into(), min as f32); + } + } + + // Semantic complexity indicator + if let Some(refs) = data.get("semantic_refs") { + if let Some(files) = refs.get("files").and_then(|f| f.as_array()) { + features.insert("semantic_file_count".into(), files.len() as f32); + } + } + + Ok(features) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::intelligence::{IntelligenceLoader, Outcome, QualitySignal}; + + fn make_acornflow_signal() -> QualitySignal { + let mut extensions = HashMap::new(); + extensions.insert( + "acornflow".to_string(), + serde_json::json!({ + "source_execution_id": "exec-456", + "workflow_name": "deploy-service", + "reasoning_chain": [ + {"nodeName": "plan", "status": "completed", "durationMs": 2300}, + {"nodeName": "implement", "status": "completed", "durationMs": 15000}, + {"nodeName": "review", "status": "completed", "durationMs": 4200} + ], + "chain_length": 3, + "semantic_refs": { + "files": [ + {"filePath": "src/auth.ts", "symbols": []}, + {"filePath": "src/api.ts", "symbols": []} + ] + }, + "quality_aggregate": {"mean": 0.85, "min": 0.7, "max": 1.0, "factorCount": 5} + }), + ); + + QualitySignal { + id: "analysis-exec-456-1234".to_string(), + task_description: "Execution analysis for exec-456".to_string(), + outcome: Outcome::Success, + quality_score: 0.85, + human_verdict: None, + quality_factors: None, + completed_at: "2025-02-21T12:00:00Z".to_string(), + extensions: Some(extensions), + } + } + + #[test] + fn trajectory_features_extraction() { + let handler = AcornflowExtensionHandler; + let signal = make_acornflow_signal(); + let ext_data = &signal.extensions.as_ref().unwrap()["acornflow"]; + + let features = handler + .extract_trajectory_features(ext_data, &signal) + .unwrap(); + + assert_eq!(features["source_execution_id"], "exec-456"); + assert_eq!(features["workflow_name"], "deploy-service"); + assert_eq!(features["chain_length"], 3); + assert_eq!(features["total_duration_ms"], 21500.0); + assert_eq!(features["semantic_file_count"], 2); + + let steps = features["chain_steps"].as_array().unwrap(); + assert_eq!(steps.len(), 3); + assert_eq!(steps[0], "plan"); + } + + #[test] + fn embedding_context_extraction() { + let handler = AcornflowExtensionHandler; + let signal = make_acornflow_signal(); + let ext_data = &signal.extensions.as_ref().unwrap()["acornflow"]; + + let context = handler.extract_embedding_context(ext_data, &signal).unwrap(); + + assert_eq!(context.len(), 2); + assert_eq!(context[0], "workflow:deploy-service"); + assert_eq!(context[1], "steps:plan->implement->review"); + } + + #[test] + fn router_features_extraction() { + let handler = AcornflowExtensionHandler; + let signal = make_acornflow_signal(); + let ext_data = &signal.extensions.as_ref().unwrap()["acornflow"]; + + let features = handler.extract_router_features(ext_data, &signal).unwrap(); + + assert!((features["chain_length"] - 3.0).abs() < f32::EPSILON); + assert!((features["quality_mean"] - 0.85).abs() < f32::EPSILON); + assert!((features["quality_min"] - 0.7).abs() < f32::EPSILON); + assert!((features["semantic_file_count"] - 2.0).abs() < f32::EPSILON); + } + + #[test] + fn end_to_end_through_loader() { + let mut loader = IntelligenceLoader::new(); + loader.register_extension_handler(Box::new(AcornflowExtensionHandler)); + + let signal = make_acornflow_signal(); + let results = loader.process_extensions(&signal); + + // Trajectory + assert_eq!(results.trajectory_features["chain_length"], 3); + assert_eq!(results.trajectory_features["workflow_name"], "deploy-service"); + + // Embedding + assert!(results.embedding_context.contains(&"workflow:deploy-service".to_string())); + + // Router + assert!((results.router_features["chain_length"] - 3.0).abs() < f32::EPSILON); + } +} diff --git a/crates/ruvllm/src/intelligence/mod.rs b/crates/ruvllm/src/intelligence/mod.rs index ba8536432..7bd763882 100644 --- a/crates/ruvllm/src/intelligence/mod.rs +++ b/crates/ruvllm/src/intelligence/mod.rs @@ -40,6 +40,8 @@ //! println!("Loaded {} signals from {} providers", signals.len(), loader.provider_count()); //! ``` +pub mod acornflow_handler; + use crate::error::Result; use serde::{Deserialize, Serialize}; use std::collections::HashMap; diff --git a/crates/ruvllm/src/lib.rs b/crates/ruvllm/src/lib.rs index 5360f44ed..ac2c7d6f7 100644 --- a/crates/ruvllm/src/lib.rs +++ b/crates/ruvllm/src/lib.rs @@ -489,9 +489,10 @@ pub use ruvector_integration::{ // Intelligence provider exports pub use intelligence::{ - FileSignalProvider, HumanVerdict, IntelligenceLoader, IntelligenceProvider, Outcome, - ProviderError, ProviderQualityWeights, ProviderResult, QualityFactors, QualitySignal, - SignalExtensionHandler, SignalExtensionResults, + acornflow_handler::AcornflowExtensionHandler, FileSignalProvider, HumanVerdict, + IntelligenceLoader, IntelligenceProvider, Outcome, ProviderError, ProviderQualityWeights, + ProviderResult, QualityFactors, QualitySignal, SignalExtensionHandler, + SignalExtensionResults, }; // Quality scoring exports From 3651295bf624828ba9ca36daacb062d0882234b5 Mon Sep 17 00:00:00 2001 From: Grant Parry Date: Tue, 24 Feb 2026 15:15:31 -0700 Subject: [PATCH 4/7] Fix formatting to pass CI code quality check Apply rustfmt to intelligence module files. Co-Authored-By: Claude Opus 4.6 --- .../src/intelligence/acornflow_handler.rs | 18 +++++++++++------- crates/ruvllm/src/intelligence/mod.rs | 9 ++------- crates/ruvllm/src/lib.rs | 3 +-- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/crates/ruvllm/src/intelligence/acornflow_handler.rs b/crates/ruvllm/src/intelligence/acornflow_handler.rs index c30f8225c..5c16a08ff 100644 --- a/crates/ruvllm/src/intelligence/acornflow_handler.rs +++ b/crates/ruvllm/src/intelligence/acornflow_handler.rs @@ -61,10 +61,7 @@ impl SignalExtensionHandler for AcornflowExtensionHandler { .iter() .filter_map(|step| step.get("nodeName").and_then(|n| n.as_str())) .collect(); - features.insert( - "chain_steps".into(), - serde_json::json!(step_names), - ); + features.insert("chain_steps".into(), serde_json::json!(step_names)); // Total duration across all steps let total_ms: f64 = chain @@ -207,7 +204,9 @@ mod tests { let signal = make_acornflow_signal(); let ext_data = &signal.extensions.as_ref().unwrap()["acornflow"]; - let context = handler.extract_embedding_context(ext_data, &signal).unwrap(); + let context = handler + .extract_embedding_context(ext_data, &signal) + .unwrap(); assert_eq!(context.len(), 2); assert_eq!(context[0], "workflow:deploy-service"); @@ -238,10 +237,15 @@ mod tests { // Trajectory assert_eq!(results.trajectory_features["chain_length"], 3); - assert_eq!(results.trajectory_features["workflow_name"], "deploy-service"); + assert_eq!( + results.trajectory_features["workflow_name"], + "deploy-service" + ); // Embedding - assert!(results.embedding_context.contains(&"workflow:deploy-service".to_string())); + assert!(results + .embedding_context + .contains(&"workflow:deploy-service".to_string())); // Router assert!((results.router_features["chain_length"] - 3.0).abs() < f32::EPSILON); diff --git a/crates/ruvllm/src/intelligence/mod.rs b/crates/ruvllm/src/intelligence/mod.rs index 7bd763882..4f57f6c04 100644 --- a/crates/ruvllm/src/intelligence/mod.rs +++ b/crates/ruvllm/src/intelligence/mod.rs @@ -940,9 +940,7 @@ mod tests { loader.register_extension_handler(Box::new(MockExtensionHandler)); assert_eq!(loader.extension_handler_count(), 1); - assert!(loader - .extension_handler_namespaces() - .contains(&"test-ns")); + assert!(loader.extension_handler_namespaces().contains(&"test-ns")); } #[test] @@ -977,10 +975,7 @@ mod tests { loader.register_extension_handler(Box::new(MockExtensionHandler)); let mut ext = HashMap::new(); - ext.insert( - "other-ns".to_string(), - serde_json::json!({"step_count": 5}), - ); + ext.insert("other-ns".to_string(), serde_json::json!({"step_count": 5})); let signal = QualitySignal { extensions: Some(ext), diff --git a/crates/ruvllm/src/lib.rs b/crates/ruvllm/src/lib.rs index ac2c7d6f7..a92b93d17 100644 --- a/crates/ruvllm/src/lib.rs +++ b/crates/ruvllm/src/lib.rs @@ -491,8 +491,7 @@ pub use ruvector_integration::{ pub use intelligence::{ acornflow_handler::AcornflowExtensionHandler, FileSignalProvider, HumanVerdict, IntelligenceLoader, IntelligenceProvider, Outcome, ProviderError, ProviderQualityWeights, - ProviderResult, QualityFactors, QualitySignal, SignalExtensionHandler, - SignalExtensionResults, + ProviderResult, QualityFactors, QualitySignal, SignalExtensionHandler, SignalExtensionResults, }; // Quality scoring exports From b7c5fcc6302a815cdfc395b6a4e3245277dee4a7 Mon Sep 17 00:00:00 2001 From: Grant Parry Date: Tue, 24 Feb 2026 15:19:02 -0700 Subject: [PATCH 5/7] Fix pre-existing CI failures on Windows and Apple Silicon Windows unit tests: PowerShell cannot parse backslash line continuations with `-- --nocapture`. Add `defaults.run.shell: bash` to the unit-tests job so all platforms use bash consistently. Apple Silicon tests: ScopedBuffer.buffer is Option, so `scoped.buffer.buffer.contents()` fails to compile. Use the existing `buffer()` accessor method which unwraps the Option. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ruvltra-tests.yml | 3 +++ crates/ruvllm/src/metal/buffers.rs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ruvltra-tests.yml b/.github/workflows/ruvltra-tests.yml index 1629b0c2f..b1c558232 100644 --- a/.github/workflows/ruvltra-tests.yml +++ b/.github/workflows/ruvltra-tests.yml @@ -36,6 +36,9 @@ jobs: unit-tests: name: Unit Tests (${{ matrix.os }}) runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash strategy: fail-fast: false matrix: diff --git a/crates/ruvllm/src/metal/buffers.rs b/crates/ruvllm/src/metal/buffers.rs index 61b10305c..ce830cfea 100644 --- a/crates/ruvllm/src/metal/buffers.rs +++ b/crates/ruvllm/src/metal/buffers.rs @@ -321,7 +321,7 @@ mod tests { let ptr = { let scoped = ScopedBuffer::new(&pool, 1000); - scoped.buffer.buffer.contents() + scoped.buffer().buffer.contents() }; // Buffer should be back in pool From 1f0ed22a208b5b282afada41d779e9d03899b60e Mon Sep 17 00:00:00 2001 From: Grant Parry Date: Tue, 24 Feb 2026 15:23:32 -0700 Subject: [PATCH 6/7] Fix rustdoc broken intra-doc links - intelligence/mod.rs: qualify `load_all_signals` as method link - bitnet/rlm_embedder.rs: escape brackets in `dst[i] += src[i]` - bitnet/rlm_refiner.rs: remove link syntax from unresolvable `TrainingTriplet` reference Co-Authored-By: Claude Opus 4.6 --- crates/ruvllm/src/bitnet/rlm_embedder.rs | 2 +- crates/ruvllm/src/bitnet/rlm_refiner.rs | 2 +- crates/ruvllm/src/intelligence/mod.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ruvllm/src/bitnet/rlm_embedder.rs b/crates/ruvllm/src/bitnet/rlm_embedder.rs index f99d1480b..56457012e 100644 --- a/crates/ruvllm/src/bitnet/rlm_embedder.rs +++ b/crates/ruvllm/src/bitnet/rlm_embedder.rs @@ -979,7 +979,7 @@ pub fn l2_normalize(v: &mut [f32]) { } } -/// Weighted vector accumulate: dst[i] += src[i] * weight. +/// Weighted vector accumulate: dst\[i\] += src\[i\] * weight. /// /// Used in context embedding computation. Auto-vectorizes. #[inline] diff --git a/crates/ruvllm/src/bitnet/rlm_refiner.rs b/crates/ruvllm/src/bitnet/rlm_refiner.rs index 84a75758b..c12c6f2b7 100644 --- a/crates/ruvllm/src/bitnet/rlm_refiner.rs +++ b/crates/ruvllm/src/bitnet/rlm_refiner.rs @@ -363,7 +363,7 @@ impl RlmRefiner { /// /// # Arguments /// - /// * `triplet_path` - Path to a JSONL file of [`TrainingTriplet`]s + /// * `triplet_path` - Path to a JSONL file of `TrainingTriplet`s /// /// # Returns /// diff --git a/crates/ruvllm/src/intelligence/mod.rs b/crates/ruvllm/src/intelligence/mod.rs index 4f57f6c04..8fd8773f0 100644 --- a/crates/ruvllm/src/intelligence/mod.rs +++ b/crates/ruvllm/src/intelligence/mod.rs @@ -439,7 +439,7 @@ impl IntelligenceProvider for FileSignalProvider { /// Aggregates quality signals from multiple registered providers. /// /// The loader maintains a list of [`IntelligenceProvider`] implementations -/// and calls them in registration order during [`load_all_signals`]. +/// and calls them in registration order during [`IntelligenceLoader::load_all_signals`]. /// /// # Zero Overhead /// From e10919406acad30c2b5192dcaae9eaefa363f878 Mon Sep 17 00:00:00 2001 From: Grant Parry Date: Tue, 24 Feb 2026 15:26:11 -0700 Subject: [PATCH 7/7] Fix copilot-setup-steps.yml YAML indentation The entire file had escalating indentation and `jobs:` was nested under `on:` instead of being a top-level key. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/copilot-setup-steps.yml | 36 +++++++++++------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 69d586c83..df03e4f76 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -3,25 +3,25 @@ name: Copilot Setup Steps on: workflow_call: - jobs: - copilot-setup: - runs-on: ubuntu-latest - environment: copilot - steps: - - name: Cleanup before git clone - run: rm -rf /home/runner/work/ruvector/ruvector - continue-on-error: true +jobs: + copilot-setup: + runs-on: ubuntu-latest + environment: copilot + steps: + - name: Cleanup before git clone + run: rm -rf /home/runner/work/ruvector/ruvector + continue-on-error: true - - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '18' + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' - - name: Install ruvector dependencies - run: npm install -g ruvector + - name: Install ruvector dependencies + run: npm install -g ruvector - - name: Verify ruvector MCP - run: npx ruvector --version + - name: Verify ruvector MCP + run: npx ruvector --version