26382a7ac6
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled
375 lines
12 KiB
Rust
375 lines
12 KiB
Rust
//! Context Column — the cortical column abstraction for data source pipelines.
|
|
//!
|
|
//! Each data source (filesystem, GitHub, Jira, DB, shell) is modeled as a
|
|
//! neocortical column with four processing layers:
|
|
//!
|
|
//! L4 (Input) — raw data ingestion, normalization → ContentChunks
|
|
//! L2/3 (Predict) — compression mode selection, predictive coding
|
|
//! L5 (Output) — verification, budget check, quality gate
|
|
//! L6 (Feedback) — top-down modulation from active task context
|
|
//!
|
|
//! Scientific basis: Mountcastle (Nature Rev Neurosci 2022) — every cortical
|
|
//! column applies the same computational template to different input modalities.
|
|
//!
|
|
//! The trait is async-ready (returns Results) so that network-backed columns
|
|
//! (GitHub API, DB queries) work naturally alongside local columns (filesystem).
|
|
|
|
use crate::core::content_chunk::ContentChunk;
|
|
|
|
/// Parameters flowing top-down from L6 to modulate processing.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct ColumnContext {
|
|
/// Active task description (modulates saliency scoring).
|
|
pub task: Option<String>,
|
|
/// Current context pressure (0.0 = relaxed, 1.0 = critical).
|
|
pub pressure: f64,
|
|
/// Token budget remaining for this delivery cycle.
|
|
pub budget_tokens: Option<usize>,
|
|
/// Compression mode hint from the mode predictor.
|
|
pub compression_hint: Option<String>,
|
|
}
|
|
|
|
/// Result of L4 (input layer) processing.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ColumnInput {
|
|
pub chunks: Vec<ContentChunk>,
|
|
pub raw_token_count: usize,
|
|
}
|
|
|
|
/// Result of L2/3 (prediction/compression layer) processing.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ColumnCompressed {
|
|
pub chunks: Vec<ContentChunk>,
|
|
pub compressed_token_count: usize,
|
|
pub compression_ratio: f64,
|
|
pub mode_used: String,
|
|
}
|
|
|
|
/// Result of L5 (output/verification layer) processing.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ColumnOutput {
|
|
pub chunks: Vec<ContentChunk>,
|
|
pub token_count: usize,
|
|
pub budget_ok: bool,
|
|
pub quality_score: f64,
|
|
/// Cross-source hints discovered during processing.
|
|
pub hints: Vec<CrossSourceHint>,
|
|
}
|
|
|
|
/// A lateral connection hint to related data in other columns.
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct CrossSourceHint {
|
|
pub source_column: String,
|
|
pub target_uri: String,
|
|
pub relation: String,
|
|
pub confidence: f64,
|
|
pub summary: String,
|
|
}
|
|
|
|
/// The cortical column trait — uniform processing pipeline for any data source.
|
|
///
|
|
/// Each implementation represents one "column" in the cortex: filesystem,
|
|
/// GitHub, Jira, PostgreSQL, etc. All columns share the same interface
|
|
/// but process different input modalities.
|
|
pub trait ContextColumn: Send + Sync {
|
|
/// Unique column identifier (matches provider ID for external columns).
|
|
fn id(&self) -> &'static str;
|
|
|
|
/// Human-readable name for discovery/logging.
|
|
fn display_name(&self) -> &'static str;
|
|
|
|
/// Whether this column is currently operational.
|
|
fn is_active(&self) -> bool;
|
|
|
|
/// **L4 (Input Layer):** Ingest raw data and produce ContentChunks.
|
|
///
|
|
/// For filesystem: read file, parse AST, extract chunks.
|
|
/// For GitHub: fetch API, parse JSON, normalize to chunks.
|
|
/// For DB: query schema/data, structure as chunks.
|
|
fn ingest(&self, query: &str, ctx: &ColumnContext) -> Result<ColumnInput, String>;
|
|
|
|
/// **L2/3 (Predictive Compression):** Compress chunks based on task context.
|
|
///
|
|
/// Uses the mode predictor (Thompson Sampling) to select the optimal
|
|
/// compression mode, then applies it. The prediction compares expected
|
|
/// vs actual information content (predictive coding).
|
|
fn compress(
|
|
&self,
|
|
input: &ColumnInput,
|
|
ctx: &ColumnContext,
|
|
) -> Result<ColumnCompressed, String> {
|
|
let mode = ctx.compression_hint.as_deref().unwrap_or("full");
|
|
let raw = input.raw_token_count;
|
|
let compressed = match mode {
|
|
"map" => (raw as f64 * 0.3) as usize,
|
|
"signatures" => (raw as f64 * 0.15) as usize,
|
|
"aggressive" => (raw as f64 * 0.1) as usize,
|
|
_ => raw,
|
|
};
|
|
Ok(ColumnCompressed {
|
|
chunks: input.chunks.clone(),
|
|
compressed_token_count: compressed.max(1),
|
|
compression_ratio: if raw > 0 {
|
|
1.0 - (compressed as f64 / raw as f64)
|
|
} else {
|
|
0.0
|
|
},
|
|
mode_used: mode.to_string(),
|
|
})
|
|
}
|
|
|
|
/// **L5 (Output + Verification):** Validate output, check budget, discover hints.
|
|
///
|
|
/// Ensures the compressed output meets quality thresholds and stays
|
|
/// within the token budget. Also discovers cross-source hints by
|
|
/// checking chunk references against the graph index.
|
|
fn verify(
|
|
&self,
|
|
compressed: &ColumnCompressed,
|
|
ctx: &ColumnContext,
|
|
) -> Result<ColumnOutput, String> {
|
|
let budget_ok = ctx
|
|
.budget_tokens
|
|
.is_none_or(|b| compressed.compressed_token_count <= b);
|
|
|
|
Ok(ColumnOutput {
|
|
chunks: compressed.chunks.clone(),
|
|
token_count: compressed.compressed_token_count,
|
|
budget_ok,
|
|
quality_score: if compressed.compression_ratio > 0.95 {
|
|
0.5
|
|
} else {
|
|
1.0
|
|
},
|
|
hints: Vec::new(),
|
|
})
|
|
}
|
|
|
|
/// Full pipeline: L4 → L2/3 → L5, with L6 context flowing top-down.
|
|
///
|
|
/// Convenience method that chains all layers. Override individual
|
|
/// layers for custom behavior per column.
|
|
fn process(&self, query: &str, ctx: &ColumnContext) -> Result<ColumnOutput, String> {
|
|
let input = self.ingest(query, ctx)?;
|
|
let compressed = self.compress(&input, ctx)?;
|
|
self.verify(&compressed, ctx)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Filesystem Column (built-in, always active)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// The filesystem column — processes local files through the cortical pipeline.
|
|
pub struct FilesystemColumn;
|
|
|
|
impl ContextColumn for FilesystemColumn {
|
|
fn id(&self) -> &'static str {
|
|
"filesystem"
|
|
}
|
|
|
|
fn display_name(&self) -> &'static str {
|
|
"Local Filesystem"
|
|
}
|
|
|
|
fn is_active(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
fn ingest(&self, query: &str, _ctx: &ColumnContext) -> Result<ColumnInput, String> {
|
|
let path = std::path::Path::new(query);
|
|
if !path.exists() {
|
|
return Err(format!("File not found: {query}"));
|
|
}
|
|
|
|
let content = std::fs::read_to_string(path).map_err(|e| format!("Read error: {e}"))?;
|
|
|
|
let token_count = content.split_whitespace().count();
|
|
let chunk = ContentChunk::from(crate::core::bm25_index::CodeChunk {
|
|
file_path: query.to_string(),
|
|
symbol_name: path
|
|
.file_name()
|
|
.and_then(|n| n.to_str())
|
|
.unwrap_or(query)
|
|
.to_string(),
|
|
kind: crate::core::bm25_index::ChunkKind::Module,
|
|
start_line: 1,
|
|
end_line: content.lines().count(),
|
|
content,
|
|
tokens: Vec::new(),
|
|
token_count,
|
|
});
|
|
|
|
Ok(ColumnInput {
|
|
chunks: vec![chunk],
|
|
raw_token_count: token_count,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Provider-backed column — wraps any `ContextProvider` as a cortical column.
|
|
pub struct ProviderColumn {
|
|
provider: std::sync::Arc<dyn crate::core::providers::ContextProvider>,
|
|
}
|
|
|
|
impl ProviderColumn {
|
|
pub fn new(provider: std::sync::Arc<dyn crate::core::providers::ContextProvider>) -> Self {
|
|
Self { provider }
|
|
}
|
|
}
|
|
|
|
impl ContextColumn for ProviderColumn {
|
|
fn id(&self) -> &'static str {
|
|
self.provider.id()
|
|
}
|
|
|
|
fn display_name(&self) -> &'static str {
|
|
self.provider.display_name()
|
|
}
|
|
|
|
fn is_active(&self) -> bool {
|
|
self.provider.is_available()
|
|
}
|
|
|
|
fn ingest(&self, query: &str, _ctx: &ColumnContext) -> Result<ColumnInput, String> {
|
|
let (action, params) = parse_column_query(query)?;
|
|
|
|
let result = self.provider.execute(&action, ¶ms)?;
|
|
let chunks = crate::core::providers::registry::result_to_chunks(&result);
|
|
|
|
let raw_tokens: usize = chunks.iter().map(|c| c.token_count).sum();
|
|
|
|
Ok(ColumnInput {
|
|
chunks,
|
|
raw_token_count: raw_tokens,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Parse a column query string into action + params.
|
|
/// Format: `action[?key=value&key=value]`
|
|
/// Example: `issues?state=open&limit=10`
|
|
fn parse_column_query(
|
|
query: &str,
|
|
) -> Result<(String, crate::core::providers::ProviderParams), String> {
|
|
let (action, query_str) = query.split_once('?').unwrap_or((query, ""));
|
|
|
|
let mut params = crate::core::providers::ProviderParams::default();
|
|
for pair in query_str.split('&') {
|
|
if pair.is_empty() {
|
|
continue;
|
|
}
|
|
let (key, value) = pair
|
|
.split_once('=')
|
|
.ok_or_else(|| format!("Invalid query param: {pair}"))?;
|
|
match key {
|
|
"state" => params.state = Some(value.to_string()),
|
|
"limit" => {
|
|
params.limit = value.parse().ok();
|
|
}
|
|
"project" => params.project = Some(value.to_string()),
|
|
"query" | "q" => params.query = Some(value.to_string()),
|
|
"id" => params.id = Some(value.to_string()),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
Ok((action.to_string(), params))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn filesystem_column_is_always_active() {
|
|
let col = FilesystemColumn;
|
|
assert!(col.is_active());
|
|
assert_eq!(col.id(), "filesystem");
|
|
}
|
|
|
|
#[test]
|
|
fn filesystem_column_ingest_nonexistent_file() {
|
|
let col = FilesystemColumn;
|
|
let ctx = ColumnContext::default();
|
|
let result = col.ingest("/nonexistent/path.rs", &ctx);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn filesystem_column_ingest_real_file() {
|
|
let col = FilesystemColumn;
|
|
let ctx = ColumnContext::default();
|
|
let result = col.ingest(file!(), &ctx);
|
|
assert!(result.is_ok());
|
|
let input = result.unwrap();
|
|
assert!(!input.chunks.is_empty());
|
|
assert!(input.raw_token_count > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn default_compress_preserves_chunks() {
|
|
let col = FilesystemColumn;
|
|
let input = ColumnInput {
|
|
chunks: vec![],
|
|
raw_token_count: 100,
|
|
};
|
|
let ctx = ColumnContext {
|
|
compression_hint: Some("map".to_string()),
|
|
..Default::default()
|
|
};
|
|
let compressed = col.compress(&input, &ctx).unwrap();
|
|
assert_eq!(compressed.mode_used, "map");
|
|
assert!(compressed.compression_ratio > 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn verify_respects_budget() {
|
|
let col = FilesystemColumn;
|
|
let compressed = ColumnCompressed {
|
|
chunks: vec![],
|
|
compressed_token_count: 500,
|
|
compression_ratio: 0.5,
|
|
mode_used: "full".into(),
|
|
};
|
|
|
|
let ctx_ok = ColumnContext {
|
|
budget_tokens: Some(1000),
|
|
..Default::default()
|
|
};
|
|
assert!(col.verify(&compressed, &ctx_ok).unwrap().budget_ok);
|
|
|
|
let ctx_over = ColumnContext {
|
|
budget_tokens: Some(100),
|
|
..Default::default()
|
|
};
|
|
assert!(!col.verify(&compressed, &ctx_over).unwrap().budget_ok);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_column_query_basic() {
|
|
let (action, params) = parse_column_query("issues?state=open&limit=10").unwrap();
|
|
assert_eq!(action, "issues");
|
|
assert_eq!(params.state.as_deref(), Some("open"));
|
|
assert_eq!(params.limit, Some(10));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_column_query_no_params() {
|
|
let (action, params) = parse_column_query("issues").unwrap();
|
|
assert_eq!(action, "issues");
|
|
assert!(params.state.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn full_pipeline_works() {
|
|
let col = FilesystemColumn;
|
|
let ctx = ColumnContext::default();
|
|
let result = col.process(file!(), &ctx);
|
|
assert!(result.is_ok());
|
|
let output = result.unwrap();
|
|
assert!(output.token_count > 0);
|
|
assert!(output.budget_ok);
|
|
}
|
|
}
|