use std::collections::BTreeMap;
use kglite::api::recipes;
use kglite::api::skills;
use kglite::api::{DirGraph, KgError};
use serde_json::Value;
pub use kglite::api::recipes::{RecipeCatalog, RecipeCatalogError, RecipeRecord};
pub use kglite::api::skills::{Delivery, SkillRecord};
pub const SKILL_NAME: &str = "code_review";
pub const RECIPE_GROUP: &str = "code_review";
pub const REFERENCES_TOOLS: &[&str] = &["cypher_query", "run_recipe_query", "read_code_source"];
pub const SKILL_DESCRIPTION: &str =
"TRIGGER when reviewing a change to an indexed codebase or answering a \
structural question about it: who calls what, what a change would break, which tests reach a \
symbol, bounded call paths, type consumers, trait implementors. Call skill(\"code_review\") \
before your first cypher_query — it names the run_recipe_query recipes that answer these \
exactly. SKIP for literal text search (use grep) and for whole-file reads that need no \
structure (read_code_source directly).";
const SKILL_BODY: &str = include_str!("../assets/code_review/skill.md");
const RECIPES_JSON: &str = include_str!("../assets/code_review/recipes.json");
pub fn skill_record() -> SkillRecord {
SkillRecord {
name: SKILL_NAME.to_string(),
description: SKILL_DESCRIPTION.to_string(),
body: SKILL_BODY.to_string(),
references_tools: REFERENCES_TOOLS
.iter()
.map(|tool| tool.to_string())
.collect(),
delivery: Delivery::Lazy,
}
}
pub fn recipe_catalog_value() -> Value {
serde_json::from_str(RECIPES_JSON)
.expect("assets/code_review/recipes.json is valid JSON (pinned by test)")
}
pub fn recipe_catalog() -> Result<RecipeCatalog, RecipeCatalogError> {
RecipeCatalog::from_manifest_value(Some(&recipe_catalog_value()))
}
pub fn recipe_records() -> Vec<RecipeRecord> {
records_from_catalog_value(&recipe_catalog_value())
}
fn records_from_catalog_value(value: &Value) -> Vec<RecipeRecord> {
let groups = value.as_object().expect("recipes.json root is a mapping");
let mut records = BTreeMap::new();
for (recipe, group) in groups {
let recipe_description = group["description"]
.as_str()
.unwrap_or_default()
.to_string();
let queries = group["queries"]
.as_object()
.expect("recipe group carries a queries mapping");
for (name, query) in queries {
records.insert(
(recipe.clone(), name.clone()),
RecipeRecord {
recipe: recipe.clone(),
name: name.clone(),
description: query["description"]
.as_str()
.unwrap_or_default()
.to_string(),
parameters: query["parameters"].clone(),
cypher: query["cypher"].as_str().unwrap_or_default().to_string(),
recipe_description: recipe_description.clone(),
tool: query["tool"].as_str().map(str::to_string),
},
);
}
}
records.into_values().collect()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AttachSummary {
pub skills: usize,
pub recipes: usize,
}
pub fn attach(graph: &mut DirGraph) -> Result<AttachSummary, Box<KgError>> {
skills::set(graph, &skill_record()).map_err(Box::new)?;
let records = recipe_records();
for record in &records {
recipes::set(graph, record).map_err(Box::new)?;
}
Ok(AttachSummary {
skills: 1,
recipes: records.len(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use kglite::api::skills::MAX_BODY_BYTES;
use kglite::api::storage::{new_dir_graph_in_mode, StorageMode};
const EXPECTED_RECIPES: &[&str] = &[
"bounded_call_path",
"caller_coverage",
"callers_page",
"direct_callees",
"target_coverage",
"trait_implementations",
"type_consumers",
];
#[test]
fn recipes_json_compiles_as_a_producer_catalogue() {
let catalog = recipe_catalog()
.unwrap_or_else(|error| panic!("recipes.json would refuse the MCP boot: {error}"));
let group = catalog
.get(RECIPE_GROUP)
.expect("the catalogue carries the code_review group");
assert_eq!(group.queries().len(), EXPECTED_RECIPES.len());
assert_eq!(catalog.recipes().len(), 1);
}
#[test]
fn every_recipe_record_validates_and_the_corpus_is_the_expected_set() {
let records = recipe_records();
let names: Vec<&str> = records.iter().map(|record| record.name.as_str()).collect();
assert_eq!(names, EXPECTED_RECIPES);
for record in &records {
assert_eq!(record.recipe, RECIPE_GROUP);
assert!(!record.recipe_description.is_empty(), "{}", record.name);
recipes::validate(record)
.unwrap_or_else(|error| panic!("recipe {} is invalid: {error}", record.name));
}
}
#[test]
fn a_declared_tool_name_survives_into_the_record() {
let mut value = recipe_catalog_value();
value[RECIPE_GROUP]["queries"]["callers_page"]["tool"] = "find_callers".into();
let records = records_from_catalog_value(&value);
let tools: Vec<(&str, Option<&str>)> = records
.iter()
.map(|record| (record.name.as_str(), record.tool.as_deref()))
.filter(|(_, tool)| tool.is_some())
.collect();
assert_eq!(tools, [("callers_page", Some("find_callers"))]);
let tagged = records.iter().find(|r| r.name == "callers_page").unwrap();
recipes::validate(tagged).unwrap_or_else(|error| panic!("{error}"));
assert!(
recipe_records().iter().all(|record| record.tool.is_none()),
"the bundled catalogue declares no tool names"
);
}
#[test]
fn skill_record_validates_fits_the_body_ceiling_and_carries_no_frontmatter() {
let record = skill_record();
skills::validate(&record)
.unwrap_or_else(|error| panic!("skill record is invalid: {error}"));
assert!(
record.body.len() <= MAX_BODY_BYTES,
"body is {} bytes, ceiling is {MAX_BODY_BYTES}",
record.body.len()
);
assert!(
!record.body.starts_with("---"),
"frontmatter belongs in the record fields, not the body"
);
assert_eq!(record.delivery, Delivery::Lazy);
assert_eq!(record.references_tools, REFERENCES_TOOLS);
}
#[test]
fn rendered_skill_fits_the_mcp_soft_size_limit() {
const MCP_SOFT_SIZE_LIMIT_BYTES: usize = 4 * 1024;
let rendered = skills::render_markdown(&skill_record());
assert!(
rendered.len() <= MCP_SOFT_SIZE_LIMIT_BYTES,
"rendered skill is {} bytes (body {}), soft limit is {MCP_SOFT_SIZE_LIMIT_BYTES}",
rendered.len(),
SKILL_BODY.len()
);
}
#[test]
fn skill_body_names_every_recipe_and_the_description_loads_it_before_cypher() {
for record in recipe_records() {
let reference = format!("`{RECIPE_GROUP}/{}`", record.name);
assert!(
SKILL_BODY.contains(&reference),
"skill body does not name {reference}"
);
}
assert!(SKILL_DESCRIPTION.contains("skill(\"code_review\")"));
assert!(SKILL_DESCRIPTION.contains("cypher_query"));
}
#[test]
fn attach_writes_the_records_into_a_fresh_graph() {
let mut graph = new_dir_graph_in_mode(StorageMode::Memory, None).unwrap();
let summary = attach(&mut graph).unwrap();
assert_eq!(
summary,
AttachSummary {
skills: 1,
recipes: EXPECTED_RECIPES.len()
}
);
assert_eq!(skills::list(&graph).len(), 1);
assert_eq!(skills::get(&graph, SKILL_NAME).unwrap(), skill_record());
let stored: Vec<String> = recipes::list(&graph)
.into_iter()
.map(|record| record.name)
.collect();
assert_eq!(stored, EXPECTED_RECIPES);
attach(&mut graph).unwrap();
assert_eq!(skills::list(&graph).len(), 1);
assert_eq!(recipes::list(&graph).len(), EXPECTED_RECIPES.len());
}
}