use super::parser::{Skill, parse_skill};
use std::collections::HashMap;
const BUILTIN_SKILLS: &[(&str, &str)] = &[
(
"falkordb-index-aware-predicates",
include_str!("../../assets/cypher-skills/falkordb-index-aware-predicates/skill.md"),
),
(
"falkordb-fulltext-search",
include_str!("../../assets/cypher-skills/falkordb-fulltext-search/skill.md"),
),
(
"falkordb-vector-search",
include_str!("../../assets/cypher-skills/falkordb-vector-search/skill.md"),
),
(
"falkordb-parameterized-queries",
include_str!("../../assets/cypher-skills/falkordb-parameterized-queries/skill.md"),
),
(
"falkordb-path-finding",
include_str!("../../assets/cypher-skills/falkordb-path-finding/skill.md"),
),
];
const WRITE_SKILL_IDS: &[&str] = &[
"create-range-indexes",
"create-and-query-fulltext-indexes",
"create-and-query-vector-indexes",
"manage-constraints",
"create-nodes-and-relationships",
"update-and-remove-properties",
"use-merge-to-avoid-duplicates",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SkillProfile {
#[default]
ReadOnly,
}
#[must_use]
pub(super) fn is_write_skill(id: &str) -> bool {
WRITE_SKILL_IDS.contains(&id)
}
#[must_use]
pub(super) fn teaches_write_cypher(content: &str) -> bool {
const WRITE_CLAUSES: &[&str] = &["CREATE", "MERGE", "DELETE", "SET", "REMOVE", "DROP"];
let mut in_code_block = false;
for segment in content.split("```") {
if in_code_block {
let normalized = segment.to_uppercase().replace(|c: char| !c.is_ascii_alphanumeric(), " ");
if normalized.split_whitespace().any(|word| WRITE_CLAUSES.contains(&word)) {
return true;
}
}
in_code_block = !in_code_block;
}
false
}
pub(super) fn builtin_skills() -> HashMap<String, Skill> {
let mut skills = HashMap::with_capacity(BUILTIN_SKILLS.len());
for (id, raw) in BUILTIN_SKILLS {
match parse_skill(id, raw) {
Ok(skill) => {
skills.insert((*id).to_string(), skill);
}
Err(e) => {
tracing::error!("Built-in skill '{id}' failed to parse: {e}");
}
}
}
skills
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builtin_skills_all_parse() {
for (id, raw) in BUILTIN_SKILLS {
let skill = parse_skill(id, raw).unwrap_or_else(|e| panic!("built-in skill '{id}' must parse: {e}"));
assert_eq!(&skill.id, id);
assert!(!skill.name.trim().is_empty(), "skill '{id}' has empty name");
assert!(
!skill.description.trim().is_empty(),
"skill '{id}' has empty description"
);
assert!(!skill.content.trim().is_empty(), "skill '{id}' has empty body");
}
}
#[test]
fn builtin_skills_are_read_only() {
for (id, raw) in BUILTIN_SKILLS {
assert!(!is_write_skill(id), "built-in skill '{id}' is a write skill");
let upper = raw.to_uppercase();
for clause in ["CREATE ", "MERGE ", "DELETE ", " SET ", "REMOVE ", "DROP "] {
assert!(
!upper.contains(clause),
"built-in skill '{id}' must not contain write clause {clause:?}"
);
}
}
}
#[test]
fn builtin_skills_loads_expected_count() {
let skills = builtin_skills();
assert_eq!(skills.len(), BUILTIN_SKILLS.len());
assert!(skills.contains_key("falkordb-fulltext-search"));
}
#[test]
fn write_skill_denylist_matches_upstream_ids() {
assert!(is_write_skill("create-range-indexes"));
assert!(is_write_skill("use-merge-to-avoid-duplicates"));
assert!(!is_write_skill("falkordb-index-aware-predicates"));
assert!(!is_write_skill("match-patterns-and-return-projections"));
}
#[test]
fn teaches_write_cypher_flags_write_code() {
assert!(teaches_write_cypher("```cypher\nCREATE (n:X)\n```"));
assert!(teaches_write_cypher("```\nMATCH (n) SET n.x = 1 RETURN n\n```"));
assert!(teaches_write_cypher("```cypher\nMATCH (n) DETACH DELETE n\n```"));
}
#[test]
fn teaches_write_cypher_ignores_prose_and_reads() {
assert!(!teaches_write_cypher(
"Writes such as CREATE, SET, DELETE, MERGE are rejected by RO_QUERY."
));
assert!(!teaches_write_cypher(
"```cypher\nMATCH (n:Person) WHERE n.age > 30 RETURN n.name\n```"
));
assert!(!teaches_write_cypher(
"```cypher\nMATCH (n) RETURN n.subset, n.created_at\n```"
));
}
}