use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::process;
use anyhow::{bail, Context, Result};
use clap::Parser;
use indexmap::IndexMap;
use serde::Deserialize;
use zilliz::model::loader::{ModelLoader, Models};
use zilliz::model::types::{Operation, Param, Resource};
#[derive(Parser)]
#[command(about = "Generate zilliz-plugin content from JSON model definitions")]
struct Args {
#[arg(long)]
output: Option<PathBuf>,
#[arg(long)]
check: Option<PathBuf>,
#[arg(long)]
skill: Option<String>,
}
#[derive(Debug, Deserialize)]
struct SkillConfigFile {
skills: IndexMap<String, SkillEntry>,
}
#[derive(Debug, Deserialize)]
struct SkillEntry {
description: String,
#[serde(default)]
resources: Vec<ResourceRef>,
#[serde(default)]
prerequisites: String,
#[serde(default)]
header: Option<String>,
#[serde(default)]
hand_written: bool,
}
#[derive(Debug, Deserialize)]
struct ResourceRef {
plane: String,
resource: String,
#[serde(default)]
section_title: Option<String>,
}
fn singularize(name: &str) -> String {
let word = name
.split('-')
.map(|w| {
let mut c = w.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().to_string() + &c.as_str().to_lowercase(),
}
})
.collect::<Vec<_>>()
.join(" ");
let lower = word.to_lowercase();
if ["alias", "index", "status", "database", "bus"].contains(&lower.as_str()) {
return word;
}
if word.ends_with("ies") {
return format!("{}y", &word[..word.len() - 3]);
}
if word.ends_with("ses") {
return word[..word.len() - 2].to_string();
}
if word.ends_with('s') && !word.ends_with("ss") {
return word[..word.len() - 1].to_string();
}
word
}
fn pluralize_title(name: &str) -> String {
let word = name
.split('-')
.map(|w| {
let mut c = w.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().to_string() + &c.as_str().to_lowercase(),
}
})
.collect::<Vec<_>>()
.join(" ");
if word.ends_with('y')
&& !word.ends_with("ay")
&& !word.ends_with("ey")
&& !word.ends_with("oy")
&& !word.ends_with("uy")
{
return format!("{}ies", &word[..word.len() - 1]);
}
if word.ends_with('s') || word.ends_with("sh") || word.ends_with("ch") || word.ends_with('x') {
return format!("{word}es");
}
format!("{word}s")
}
fn article(word: &str) -> &'static str {
let lower = word.to_lowercase();
if lower.starts_with("user") || lower.starts_with("uni") || lower.starts_with("use") {
return "a";
}
if matches!(
lower.as_bytes().first(),
Some(b'a' | b'e' | b'i' | b'o' | b'u')
) {
"an"
} else {
"a"
}
}
fn title_templates() -> HashMap<&'static str, &'static str> {
let mut m = HashMap::new();
m.insert("list", "List {Rs}");
m.insert("describe", "Describe {a} {R}");
m.insert("create", "Create {a} {R}");
m.insert("delete", "Delete {a} {R}");
m.insert("drop", "Drop {a} {R}");
m.insert("modify", "Modify {a} {R}");
m.insert("suspend", "Suspend {a} {R}");
m.insert("resume", "Resume {a} {R}");
m.insert("load", "Load {a} {R}");
m.insert("release", "Release {a} {R}");
m.insert("rename", "Rename {a} {R}");
m.insert("has", "Check if {a} {R} Exists");
m.insert("flush", "Flush {a} {R}");
m.insert("compact", "Compact {a} {R}");
m.insert("insert", "Insert Vectors");
m.insert("upsert", "Upsert Vectors");
m.insert("search", "Vector Search");
m.insert("hybrid-search", "Hybrid Search");
m.insert("query", "Query by Filter");
m.insert("get", "Get by ID");
m.insert("start", "Start an Import Job");
m.insert("status", "Check Import Status");
m.insert("export", "Export a Backup");
m.insert("providers", "List Cloud Providers");
m.insert("regions", "List Regions");
m.insert("alter", "Alter an Alias");
m.insert("get-load-state", "Get Load State");
m.insert("get-stats", "Get Statistics");
m.insert("restore-cluster", "Restore to a New Cluster");
m.insert("restore-collection", "Restore Specific Collections");
m.insert("describe-policy", "Describe Backup Policy");
m.insert("update-policy", "Update Backup Policy");
m.insert("update-password", "Update Password");
m.insert("grant-role", "Grant a Role to a User");
m.insert("revoke-role", "Revoke a Role from a User");
m.insert("grant-privilege", "Grant a Privilege");
m.insert("revoke-privilege", "Revoke a Privilege");
m.insert("upgrade", "Upgrade a Project");
m
}
fn format_operation_title(op_name: &str, description: &str, resource_name: &str) -> String {
let templates = title_templates();
let s = singularize(resource_name);
let p = pluralize_title(resource_name);
let a = article(&s);
if let Some(tmpl) = templates.get(op_name) {
return tmpl
.replace("{R}", &s)
.replace("{Rs}", &p)
.replace("{a}", a);
}
let desc = description.trim().trim_end_matches('.');
if !desc.is_empty() {
let cleaned = if let Some(pos) = desc.rfind('(') {
desc[..pos].trim()
} else {
desc
};
if !cleaned.is_empty() {
return cleaned.to_string();
}
}
op_name
.split('-')
.map(|w| {
let mut c = w.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().to_string() + c.as_str(),
}
})
.collect::<Vec<_>>()
.join(" ")
}
type ExampleKey = (&'static str, &'static str, &'static str);
fn json_param_examples() -> HashMap<ExampleKey, &'static str> {
let mut m: HashMap<ExampleKey, &'static str> = HashMap::new();
m.insert(
("vector", "insert", "data"),
"'[{\"id\": 1, \"vector\": [0.1, 0.2, ...], \"text\": \"hello\"}]'",
);
m.insert(
("vector", "upsert", "data"),
"'[{\"id\": 1, \"vector\": [0.1, 0.2, ...], \"text\": \"hello\"}]'",
);
m.insert(("vector", "search", "data"), "'[[0.1, 0.2, 0.3, ...]]'");
m.insert(
("vector", "hybrid-search", "search"),
"'[{\"data\": [[0.1, ...]], \"annsField\": \"dense_vector\", \"limit\": 10}, {\"data\": [[\"search text\"]], \"annsField\": \"sparse_vector\", \"limit\": 10}]'",
);
m.insert(
("vector", "hybrid-search", "rerank"),
"'{\"strategy\": \"rrf\", \"params\": {\"k\": 60}}'",
);
m.insert(("vector", "get", "id"), "'[1, 2, 3]'");
m.insert(
("partition", "load", "partitionNames"),
"'[\"partition1\", \"partition2\"]'",
);
m.insert(
("partition", "release", "partitionNames"),
"'[\"partition1\", \"partition2\"]'",
);
m.insert(("*", "*", "outputFields"), "'[\"field1\", \"field2\"]'");
m
}
type BodyKey = (&'static str, &'static str);
fn body_examples() -> HashMap<BodyKey, &'static str> {
let mut m: HashMap<BodyKey, &'static str> = HashMap::new();
m.insert(
("collection", "create"),
"--body '{\"schema\": {\"fields\": [{\"fieldName\": \"id\", \"dataType\": \"Int64\", \"isPrimary\": true}, {\"fieldName\": \"vector\", \"dataType\": \"FloatVector\", \"elementTypeParams\": {\"dim\": \"768\"}}]}}'",
);
m.insert(
("index", "create"),
"--body '{\"indexParams\": [{\"fieldName\": \"vector\", \"indexType\": \"AUTOINDEX\", \"metricType\": \"COSINE\"}]}'",
);
m.insert(
("import", "start"),
"--body '{\"files\": [[\"s3://bucket/path/data.parquet\"]]}'",
);
m.insert(
("cluster", "modify"),
"--body '{\"cuSize\": 2, \"replica\": 2}'",
);
m.insert(
("backup", "restore-collection"),
"--body '{\"collections\": [{\"source\": \"col1\", \"target\": \"col1_restored\"}]}'",
);
m.insert(("database", "create"), "--body '{\"properties\": {}}'");
m
}
fn strip_parentheticals(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut depth = 0;
for ch in s.chars() {
match ch {
'(' => depth += 1,
')' if depth > 0 => {
depth -= 1;
}
_ if depth == 0 => result.push(ch),
_ => {}
}
}
result.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn build_placeholder(param: &Param, resource_name: &str, op_name: &str) -> String {
if param.param_type == "boolean" {
return "<true|false>".to_string();
}
if let Some(choices) = ¶m.choices {
return format!("<{}>", choices.join("|"));
}
if param.param_type == "array" || param.param_type == "object" {
let examples = json_param_examples();
if let Some(ex) = examples
.get(&(resource_name, op_name, param.name.as_str()))
.or_else(|| examples.get(&("*", "*", param.name.as_str())))
{
return ex.to_string();
}
return if param.param_type == "array" {
"'[...]'".to_string()
} else {
"'{...}'".to_string()
};
}
if let Some(desc) = ¶m.description {
let clean = strip_parentheticals(desc);
let clean = if let Some(pos) = clean.to_lowercase().find("e.g.") {
clean[..pos].to_string()
} else {
clean
};
let clean = clean.trim().to_string();
if !clean.is_empty() && !clean.contains('|') && !clean.contains(',') && clean.len() <= 30 {
let placeholder = clean
.to_lowercase()
.split_whitespace()
.collect::<Vec<_>>()
.join("-");
let placeholder: String = placeholder
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '*')
.collect();
if !placeholder.is_empty() {
return format!("<{placeholder}>");
}
}
}
let flag = param.cli_flag();
let name = flag.trim_start_matches('-');
format!("<{name}>")
}
fn build_flag(param: &Param, resource_name: &str, op_name: &str) -> String {
let flag = param.cli_flag();
let placeholder = build_placeholder(param, resource_name, op_name);
format!("{flag} {placeholder}")
}
const MULTILINE_THRESHOLD: usize = 2;
fn format_command(prefix: &str, flags: &[String]) -> String {
if flags.len() <= MULTILINE_THRESHOLD {
let mut parts = vec![prefix.to_string()];
parts.extend(flags.iter().cloned());
return parts.join(" ");
}
let mut lines = vec![format!("{prefix} \\")];
for (i, flag) in flags.iter().enumerate() {
if i == flags.len() - 1 {
lines.push(format!(" {flag}"));
} else {
lines.push(format!(" {flag} \\"));
}
}
lines.join("\n")
}
fn is_required(param: &Param) -> bool {
param.required || param.required_unless.is_some()
}
fn is_pagination_param(param: &Param) -> bool {
matches!(param.name.as_str(), "pageSize" | "currentPage")
}
fn build_command_block(resource_name: &str, op_name: &str, operation: &Operation) -> String {
let mut lines: Vec<String> = Vec::new();
let description = operation.description.as_deref().unwrap_or("");
let title = format_operation_title(op_name, description, resource_name);
lines.push(format!("### {title}"));
lines.push(String::new());
let has_body_param = operation.body_param.is_some();
let has_pagination = operation.pagination.is_some();
let required_params: Vec<&Param> = operation
.params
.iter()
.filter(|p| is_required(p) && !is_pagination_param(p))
.collect();
let optional_params: Vec<&Param> = operation
.params
.iter()
.filter(|p| !is_required(p) && !is_pagination_param(p))
.collect();
let prefix = format!("zilliz {resource_name} {op_name}");
let required_flags: Vec<String> = required_params
.iter()
.map(|p| build_flag(p, resource_name, op_name))
.collect();
let cmd = format_command(&prefix, &required_flags);
lines.push("```bash".to_string());
lines.push(cmd);
if !optional_params.is_empty() {
let opt_strs: Vec<String> = optional_params
.iter()
.map(|p| build_flag(p, resource_name, op_name))
.collect();
if opt_strs.len() <= 2 {
lines.push(format!("# Optional: {}", opt_strs.join(", ")));
} else {
lines.push("# Optional:".to_string());
for opt in &opt_strs {
lines.push(format!("# {opt}"));
}
}
}
if has_body_param {
let body_exs = body_examples();
if let Some(ex) = body_exs.get(&(resource_name, op_name)) {
lines.push(format!("# Or use raw JSON: {ex}"));
} else {
let flag = operation.body_param.as_deref().unwrap_or("--body");
lines.push(format!("# Or use raw JSON: {flag} '{{...}}'"));
}
}
if has_pagination {
lines.push("# Pagination: --page-size <n> --page <n>".to_string());
lines.push("# Fetch all pages: --all".to_string());
}
lines.push("```".to_string());
if operation.dedicated_only {
lines.push(String::new());
lines.push(format!("*{description}*"));
}
lines.join("\n")
}
fn build_resource_commands(
resource_name: &str,
resource: &Resource,
section_title: Option<&str>,
) -> String {
let mut lines: Vec<String> = Vec::new();
if let Some(title) = section_title {
lines.push(format!("### {title}"));
lines.push(String::new());
}
for (op_name, operation) in &resource.operations {
let mut block = build_command_block(resource_name, op_name, operation);
if section_title.is_some() {
block = block.replacen("### ", "#### ", 1);
}
lines.push(block);
lines.push(String::new());
}
let result = lines.join("\n");
result.trim_end_matches('\n').to_string()
}
fn load_overlay(overlays_dir: &Path, skill_name: &str, filename: &str) -> Option<String> {
let path = overlays_dir.join(skill_name).join(filename);
fs::read_to_string(&path)
.ok()
.map(|s| s.trim_end_matches('\n').to_string())
}
fn generate_skill(
skill_name: &str,
entry: &SkillEntry,
models: &Models,
overlays_dir: &Path,
) -> Result<String> {
if entry.hand_written {
let path = overlays_dir.join(skill_name).join("SKILL.md");
let content = fs::read_to_string(&path).with_context(|| {
format!(
"Hand-written skill '{skill_name}' missing overlay at {}",
path.display()
)
})?;
validate_overlay_invariants(skill_name, &content, overlays_dir)?;
return Ok(content);
}
let mut parts: Vec<String> = Vec::new();
parts.push("---".to_string());
parts.push(format!("name: {skill_name}"));
parts.push(format!("description: {}", entry.description));
parts.push("---".to_string());
parts.push(String::new());
let prereqs = entry.prerequisites.trim();
if !prereqs.is_empty() {
parts.push("## Prerequisites".to_string());
parts.push(String::new());
parts.push(prereqs.to_string());
parts.push(String::new());
}
parts.push("## Commands Reference".to_string());
parts.push(String::new());
if let Some(header) = &entry.header {
parts.push(header.trim().to_string());
parts.push(String::new());
}
if let Some(extra) = load_overlay(overlays_dir, skill_name, "extra_commands.md") {
parts.push(extra);
parts.push(String::new());
}
for res_ref in &entry.resources {
let model = match res_ref.plane.as_str() {
"control" => &models.control_plane,
"data" => &models.data_plane,
other => {
eprintln!("Warning: plane '{other}' not found for skill '{skill_name}'");
continue;
}
};
let resource = match model.resources.get(&res_ref.resource) {
Some(r) => r,
None => {
eprintln!(
"Warning: resource '{}' not found in {}-plane.json for skill '{skill_name}'",
res_ref.resource, res_ref.plane
);
continue;
}
};
let commands_md = build_resource_commands(
&res_ref.resource,
resource,
res_ref.section_title.as_deref(),
);
parts.push(commands_md);
parts.push(String::new());
}
if let Some(guidance) = load_overlay(overlays_dir, skill_name, "guidance.md") {
parts.push(guidance);
parts.push(String::new());
}
let result = parts.join("\n");
let output = format!("{}\n", result.trim_end_matches('\n'));
validate_overlay_invariants(skill_name, &output, overlays_dir)?;
Ok(output)
}
fn validate_overlay_invariants(
skill_name: &str,
rendered: &str,
overlays_dir: &Path,
) -> Result<()> {
match skill_name {
"collection" => {
let extra_path = overlays_dir.join("collection").join("extra_commands.md");
if !extra_path.exists() {
bail!(
"collection skill is missing `{}` -- the `zilliz collection metrics` \
section must live there so it surfaces in the generated SKILL.md",
extra_path.display()
);
}
if !rendered.contains("## Collection metrics") {
bail!(
"collection skill overlay must contain a `## Collection metrics` section \
documenting the `zilliz collection metrics` subcommand"
);
}
const REQUIRED_FLAGS: &[&str] = &[
"--collection-name",
"--metric",
"--cluster-id",
"--period",
"--start",
"--end",
"--granularity",
];
for flag in REQUIRED_FLAGS {
if !rendered.contains(flag) {
bail!(
"collection skill overlay must document flag `{flag}` for \
`zilliz collection metrics`"
);
}
}
const CLUSTER_ONLY: &[&str] = &[
"CU_COMPUTATION",
"CU_CAPACITY",
"CU_SIZE",
"REPLICA_COUNT",
"STORAGE",
"COLLECTIONS",
"SLOW_QUERIES",
"READ_VCU",
"WRITE_VCU",
];
for metric in CLUSTER_ONLY {
if !rendered.contains(metric) {
bail!(
"collection skill overlay must list cluster-only metric `{metric}` \
so the plugin warns users it is rejected under `zilliz collection metrics`"
);
}
}
const HYBRID_ALIASES: &[&str] = &[
"HYBRID_SEARCH_QPS",
"HYBRID_SEARCH_LATENCY_AVG",
"HYBRID_SEARCH_LATENCY_P99",
"HYBRID_SEARCH_FAIL_RATE",
];
for alias in HYBRID_ALIASES {
if !rendered.contains(alias) {
bail!("collection skill overlay must document hybrid-search alias `{alias}`");
}
}
}
"monitoring" if !rendered.contains("zilliz collection metrics") => {
bail!(
"monitoring skill overlay must cross-reference `zilliz collection metrics` \
so users exploring per-collection observations are routed correctly"
);
}
_ => {}
}
Ok(())
}
fn find_project_root() -> Result<PathBuf> {
let mut dir = std::env::current_dir()?;
loop {
if dir.join("Cargo.toml").exists() && dir.join("src").exists() {
return Ok(dir);
}
if !dir.pop() {
bail!("Could not find project root (no Cargo.toml found in ancestors)");
}
}
}
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let file_type = entry.file_type()?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if file_type.is_dir() {
copy_dir_recursive(&src_path, &dst_path)?;
} else {
fs::copy(&src_path, &dst_path)?;
}
}
Ok(())
}
fn main() -> Result<()> {
let args = Args::parse();
let project_root = find_project_root()?;
let scripts_dir = project_root.join("scripts");
let overlays_dir = scripts_dir.join("skill_overlays");
let config_path = scripts_dir.join("skill_config.yaml");
let template_dir = scripts_dir.join("plugin_template");
let config_str =
fs::read_to_string(&config_path).context("Failed to read skill_config.yaml")?;
let config: SkillConfigFile =
serde_yaml::from_str(&config_str).context("Failed to parse skill_config.yaml")?;
let models = ModelLoader::load_builtin()?;
let skills: IndexMap<String, &SkillEntry> = if let Some(ref name) = args.skill {
if let Some(entry) = config.skills.get(name) {
let mut m = IndexMap::new();
m.insert(name.clone(), entry);
m
} else {
bail!("Skill '{name}' not found in config");
}
} else {
config.skills.iter().map(|(k, v)| (k.clone(), v)).collect()
};
let mut drift_found = false;
if let Some(ref check_dir) = args.check {
for (skill_name, entry) in &skills {
let content = generate_skill(skill_name, entry, &models, &overlays_dir)?;
let existing_path = check_dir.join("skills").join(skill_name).join("SKILL.md");
if !existing_path.exists() {
println!("MISSING: {}", existing_path.display());
drift_found = true;
continue;
}
let existing = fs::read_to_string(&existing_path)?;
if existing != content {
println!("DRIFT: {}", existing_path.display());
drift_found = true;
} else {
println!("OK: {}", existing_path.display());
}
}
if template_dir.exists() {
check_template_files(&template_dir, check_dir, &mut drift_found)?;
}
if drift_found {
eprintln!("\nDrift detected! Run the generator to update plugin content.");
process::exit(1);
}
} else if let Some(ref output_dir) = args.output {
if template_dir.exists() {
copy_dir_recursive(&template_dir, output_dir)?;
println!("Copied template files to {}", output_dir.display());
}
for (skill_name, entry) in &skills {
let content = generate_skill(skill_name, entry, &models, &overlays_dir)?;
let out_dir = output_dir.join("skills").join(skill_name);
fs::create_dir_all(&out_dir)?;
let out_path = out_dir.join("SKILL.md");
fs::write(&out_path, &content)?;
println!("Generated: {}", out_path.display());
}
} else {
let separator = "=".repeat(60);
for (skill_name, entry) in &skills {
let content = generate_skill(skill_name, entry, &models, &overlays_dir)?;
println!("{separator}");
println!("# skills/{skill_name}/SKILL.md");
println!("{separator}");
print!("{content}");
println!();
}
}
Ok(())
}
fn check_template_files(template_dir: &Path, check_dir: &Path, drift: &mut bool) -> Result<()> {
for entry in fs::read_dir(template_dir)? {
let entry = entry?;
let src_path = entry.path();
let rel = entry.file_name();
let dst_path = check_dir.join(&rel);
if src_path.is_dir() {
if dst_path.exists() {
check_template_files(&src_path, &dst_path, drift)?;
} else {
println!("MISSING: {}", dst_path.display());
*drift = true;
}
} else {
if !dst_path.exists() {
println!("MISSING: {}", dst_path.display());
*drift = true;
continue;
}
let src_content = fs::read_to_string(&src_path)?;
let dst_content = fs::read_to_string(&dst_path)?;
if src_content != dst_content {
println!("DRIFT: {}", dst_path.display());
*drift = true;
} else {
println!("OK: {}", dst_path.display());
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn load_config_and_models() -> (SkillConfigFile, Models, PathBuf) {
let root = find_project_root().expect("project root");
let scripts_dir = root.join("scripts");
let overlays_dir = scripts_dir.join("skill_overlays");
let config_str = fs::read_to_string(scripts_dir.join("skill_config.yaml"))
.expect("read skill_config.yaml");
let config: SkillConfigFile =
serde_yaml::from_str(&config_str).expect("parse skill_config.yaml");
let models = ModelLoader::load_builtin().expect("load builtin models");
(config, models, overlays_dir)
}
fn generate(skill_name: &str) -> String {
let (config, models, overlays_dir) = load_config_and_models();
let entry = config
.skills
.get(skill_name)
.unwrap_or_else(|| panic!("skill '{skill_name}' missing from config"));
generate_skill(skill_name, entry, &models, &overlays_dir)
.unwrap_or_else(|e| panic!("generate_skill({skill_name}) failed: {e}"))
}
#[test]
fn collection_skill_documents_metrics_flags() {
let content = generate("collection");
assert!(
content.contains("## Collection metrics"),
"missing `## Collection metrics` section"
);
for flag in [
"--collection-name",
"--metric",
"--cluster-id",
"--period",
"--start",
"--end",
"--granularity",
] {
assert!(
content.contains(flag),
"collection SKILL.md missing flag `{flag}`"
);
}
assert!(
content.contains("zilliz collection metrics"),
"collection SKILL.md missing `zilliz collection metrics` invocation"
);
}
#[test]
fn collection_skill_lists_cluster_only_metrics_as_rejected() {
let content = generate("collection");
for metric in [
"CU_COMPUTATION",
"CU_CAPACITY",
"CU_SIZE",
"REPLICA_COUNT",
"STORAGE",
"COLLECTIONS",
"SLOW_QUERIES",
"READ_VCU",
"WRITE_VCU",
] {
assert!(
content.contains(metric),
"collection SKILL.md missing cluster-only metric `{metric}`"
);
}
assert!(
content.contains("cluster-scope only")
|| content.contains("Cluster only")
|| content.contains("Cluster-only"),
"collection SKILL.md must explain that cluster-only metrics are rejected"
);
}
#[test]
fn monitoring_skill_cross_references_collection_metrics() {
let content = generate("monitoring");
assert!(
content.contains("zilliz collection metrics"),
"monitoring SKILL.md must cross-reference `zilliz collection metrics`"
);
}
}