chore(engine): remove unused skill_selector.rs

Rust-side skill selection was moved to the Python orchestrator in
7f87d179. This module had no production callers — only its own tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
2026-03-27 20:04:01 -07:00
co-authored by Claude Opus 4.6
parent a7e31a0e60
commit 49e83dec5e
2 changed files with 0 additions and 348 deletions
@@ -8,7 +8,6 @@ pub mod lease;
pub mod planner;
pub mod policy;
pub mod registry;
pub mod skill_selector;
pub mod skill_tracker;
pub use lease::LeaseManager;
@@ -1,347 +0,0 @@
//! Skill selection for the v2 engine (Rust-side).
//!
//! **Superseded** — skill selection moved to the Python orchestrator
//! (`orchestrator/default.py`: `select_skills()`, `score_skill()`).
//! This module is no longer called from production code. It remains for
//! reference and its unit tests. Remove when the Python implementation
//! is fully validated and this Rust fallback is no longer needed.
//!
//! Bridges `MemoryDoc` (the engine's storage primitive) to `LoadedSkill`
//! (the skills crate's scoring primitive), then delegates to the shared
//! deterministic scoring pipeline.
use ironclaw_skills::selector::prefilter_skills;
use ironclaw_skills::types::{LoadedSkill, SkillManifest, SkillSource, SkillTrust};
use ironclaw_skills::v2::{V2SkillMetadata, V2SkillSource};
use crate::types::error::EngineError;
use crate::types::memory::{DocId, DocType, MemoryDoc};
/// A skill prepared for v2 selection.
///
/// Holds both the shared `LoadedSkill` (used by the scoring algorithm) and
/// the v2-specific `V2SkillMetadata` (code snippets, metrics, versioning).
#[derive(Debug, Clone)]
pub struct PreparedSkill {
/// The MemoryDoc ID this skill was loaded from.
pub doc_id: DocId,
/// Shared skill type used by `prefilter_skills()`.
pub loaded: LoadedSkill,
/// V2-specific metadata (code snippets, metrics, version).
pub metadata: V2SkillMetadata,
}
/// Result of skill selection for a thread.
#[derive(Debug)]
pub struct SkillSelection {
/// Selected skills, ordered by score descending.
pub skills: Vec<PreparedSkill>,
/// Minimum trust level across selected skills (for attenuation).
pub min_trust: SkillTrust,
}
/// Selects relevant skills for a thread from project MemoryDocs.
///
/// Loads `DocType::Skill` docs once at construction, pre-compiles regex
/// patterns, and provides a fast `select()` method for per-thread scoring.
pub struct SkillSelector {
prepared: Vec<PreparedSkill>,
}
impl SkillSelector {
/// Build from a project's skill MemoryDocs.
///
/// Deserializes `V2SkillMetadata` from each doc's `metadata` JSON, constructs
/// a `LoadedSkill` for the shared scoring algorithm (compiles regex, lowercases
/// keywords). Malformed docs are skipped with a warning.
pub fn from_docs(docs: Vec<MemoryDoc>) -> Result<Self, EngineError> {
let mut prepared = Vec::new();
for doc in docs {
if doc.doc_type != DocType::Skill {
continue;
}
let meta: V2SkillMetadata = match serde_json::from_value(doc.metadata.clone()) {
Ok(m) => m,
Err(e) => {
tracing::warn!(
doc_id = %doc.id.0,
title = %doc.title,
"Skipping skill doc with invalid metadata: {e}"
);
continue;
}
};
let loaded = metadata_to_loaded_skill(&meta, &doc.content);
prepared.push(PreparedSkill {
doc_id: doc.id,
loaded,
metadata: meta,
});
}
Ok(Self { prepared })
}
/// Select relevant skills for a query string.
///
/// Delegates scoring to `ironclaw_skills::selector::prefilter_skills()`,
/// then applies confidence factors for extracted skills and wraps the
/// result as a `SkillSelection`.
pub fn select(
&self,
query: &str,
max_candidates: usize,
max_context_tokens: usize,
) -> SkillSelection {
if self.prepared.is_empty() {
return SkillSelection {
skills: vec![],
min_trust: SkillTrust::Trusted,
};
}
// Build a slice of LoadedSkill references for the shared scorer.
let loaded_skills: Vec<LoadedSkill> = self
.prepared
.iter()
.map(|p| {
// Apply confidence factor by adjusting the token budget hint.
// The actual scoring happens in prefilter_skills; confidence
// doesn't change keyword scores but we track it for later use.
p.loaded.clone()
})
.collect();
let selected_refs = prefilter_skills(query, &loaded_skills, max_candidates, max_context_tokens);
// Map selected LoadedSkill refs back to PreparedSkills by matching names.
let selected_names: Vec<&str> = selected_refs.iter().map(|s| s.name()).collect();
let skills: Vec<PreparedSkill> = selected_names
.iter()
.filter_map(|name| self.prepared.iter().find(|p| p.loaded.name() == *name))
.cloned()
.collect();
let min_trust = skills
.iter()
.map(|s| s.metadata.trust)
.min()
.unwrap_or(SkillTrust::Trusted);
SkillSelection { skills, min_trust }
}
/// Returns true if no skills are loaded.
pub fn is_empty(&self) -> bool {
self.prepared.is_empty()
}
/// Number of loaded skills.
pub fn len(&self) -> usize {
self.prepared.len()
}
}
/// Convert v2 metadata + content into a `LoadedSkill` for the shared scorer.
fn metadata_to_loaded_skill(meta: &V2SkillMetadata, content: &str) -> LoadedSkill {
let compiled_patterns = LoadedSkill::compile_patterns(&meta.activation.patterns);
let lowercased_keywords = meta
.activation
.keywords
.iter()
.map(|k| k.to_lowercase())
.collect();
let lowercased_exclude_keywords = meta
.activation
.exclude_keywords
.iter()
.map(|k| k.to_lowercase())
.collect();
let lowercased_tags = meta
.activation
.tags
.iter()
.map(|t| t.to_lowercase())
.collect();
let trust = meta.trust;
let source = match meta.source {
V2SkillSource::Authored | V2SkillSource::Migrated => {
SkillSource::User(std::path::PathBuf::from("(v2-memory)"))
}
V2SkillSource::Extracted => SkillSource::User(std::path::PathBuf::from("(v2-extracted)")),
};
LoadedSkill {
manifest: SkillManifest {
name: meta.name.clone(),
version: meta.version.to_string(),
description: meta.description.clone(),
activation: meta.activation.clone(),
credentials: vec![],
metadata: None,
},
prompt_content: content.to_string(),
trust,
source,
content_hash: meta.content_hash.clone(),
compiled_patterns,
lowercased_keywords,
lowercased_exclude_keywords,
lowercased_tags,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::memory::MemoryDoc;
use crate::types::project::ProjectId;
use ironclaw_skills::types::ActivationCriteria;
use ironclaw_skills::v2::{CodeSnippet, SkillMetrics};
fn make_skill_doc(
name: &str,
keywords: &[&str],
content: &str,
project_id: ProjectId,
) -> MemoryDoc {
let meta = V2SkillMetadata {
name: name.to_string(),
version: 1,
description: format!("{name} skill"),
activation: ActivationCriteria {
keywords: keywords.iter().map(|s| s.to_string()).collect(),
max_context_tokens: 1000,
..Default::default()
},
source: V2SkillSource::Authored,
trust: SkillTrust::Trusted,
code_snippets: vec![],
metrics: SkillMetrics::default(),
parent_version: None,
content_hash: String::new(),
};
let mut doc = MemoryDoc::new(project_id, DocType::Skill, format!("skill:{name}"), content);
doc.metadata = serde_json::to_value(&meta).unwrap();
doc
}
#[test]
fn test_from_docs_filters_non_skill_docs() {
let pid = ProjectId::new();
let docs = vec![
MemoryDoc::new(pid, DocType::Lesson, "a lesson", "lesson content"),
make_skill_doc("github", &["issues", "github"], "GitHub skill prompt", pid),
];
let selector = SkillSelector::from_docs(docs).unwrap();
assert_eq!(selector.len(), 1);
}
#[test]
fn test_from_docs_skips_malformed_metadata() {
let pid = ProjectId::new();
let mut bad_doc =
MemoryDoc::new(pid, DocType::Skill, "skill:broken", "broken skill prompt");
bad_doc.metadata = serde_json::json!("not an object");
let docs = vec![bad_doc];
let selector = SkillSelector::from_docs(docs).unwrap();
assert!(selector.is_empty());
}
#[test]
fn test_select_returns_matching_skills() {
let pid = ProjectId::new();
let docs = vec![
make_skill_doc("github", &["issues", "github", "pull"], "GitHub integration", pid),
make_skill_doc("cooking", &["recipe", "cook", "bake"], "Cooking helper", pid),
];
let selector = SkillSelector::from_docs(docs).unwrap();
let selection = selector.select("show me open github issues", 3, 4000);
assert_eq!(selection.skills.len(), 1);
assert_eq!(selection.skills[0].metadata.name, "github");
}
#[test]
fn test_select_empty_query() {
let pid = ProjectId::new();
let docs = vec![make_skill_doc("test", &["test"], "Test skill", pid)];
let selector = SkillSelector::from_docs(docs).unwrap();
let selection = selector.select("", 3, 4000);
assert!(selection.skills.is_empty());
}
#[test]
fn test_select_respects_budget() {
let pid = ProjectId::new();
let mut doc1 = make_skill_doc("big", &["test"], "Big skill prompt", pid);
let mut meta1: V2SkillMetadata = serde_json::from_value(doc1.metadata.clone()).unwrap();
meta1.activation.max_context_tokens = 3000;
doc1.metadata = serde_json::to_value(&meta1).unwrap();
let mut doc2 = make_skill_doc("also_big", &["test"], "Also big prompt", pid);
let mut meta2: V2SkillMetadata = serde_json::from_value(doc2.metadata.clone()).unwrap();
meta2.activation.max_context_tokens = 3000;
doc2.metadata = serde_json::to_value(&meta2).unwrap();
let selector = SkillSelector::from_docs(vec![doc1, doc2]).unwrap();
// Budget of 4000 fits only one 3000-token skill
let selection = selector.select("test", 5, 4000);
assert_eq!(selection.skills.len(), 1);
}
#[test]
fn test_min_trust_computed() {
let pid = ProjectId::new();
let mut doc = make_skill_doc("installed", &["test"], "Installed skill", pid);
let mut meta: V2SkillMetadata = serde_json::from_value(doc.metadata.clone()).unwrap();
meta.trust = SkillTrust::Installed;
doc.metadata = serde_json::to_value(&meta).unwrap();
let selector = SkillSelector::from_docs(vec![doc]).unwrap();
let selection = selector.select("test", 3, 4000);
assert_eq!(selection.skills.len(), 1);
assert_eq!(selection.min_trust, SkillTrust::Installed);
}
#[test]
fn test_code_snippets_preserved() {
let pid = ProjectId::new();
let mut doc = make_skill_doc("snippets", &["fetch"], "Skill with code", pid);
let mut meta: V2SkillMetadata = serde_json::from_value(doc.metadata.clone()).unwrap();
meta.code_snippets = vec![CodeSnippet {
name: "fetch_data".to_string(),
code: "def fetch_data(): pass".to_string(),
description: "Fetches data".to_string(),
}];
doc.metadata = serde_json::to_value(&meta).unwrap();
let selector = SkillSelector::from_docs(vec![doc]).unwrap();
let selection = selector.select("fetch some data", 3, 4000);
assert_eq!(selection.skills.len(), 1);
assert_eq!(selection.skills[0].metadata.code_snippets.len(), 1);
assert_eq!(selection.skills[0].metadata.code_snippets[0].name, "fetch_data");
}
#[test]
fn test_empty_selector() {
let selector = SkillSelector::from_docs(vec![]).unwrap();
assert!(selector.is_empty());
assert_eq!(selector.len(), 0);
let selection = selector.select("anything", 3, 4000);
assert!(selection.skills.is_empty());
assert_eq!(selection.min_trust, SkillTrust::Trusted);
}
}