use std::path::Path;
use anyhow::{bail, Context, Result};
use serde::Deserialize;
use super::types::{
OutputFormat, RuleResult, ValidateArgs, ValidationReport, ValidationSummary, Violation,
};
pub(super) fn cmd_validate(args: ValidateArgs) -> Result<()> {
let kg_dir = args.repo.join(".khive/kg");
if !kg_dir.exists() {
bail!(
"KG directory not found: {}. Run `kkernel kg init` first.",
kg_dir.display()
);
}
let entities_path = kg_dir.join("entities.ndjson");
let edges_path = kg_dir.join("edges.ndjson");
let entities = count_ndjson_lines(&entities_path).unwrap_or(0);
let edges = count_ndjson_lines(&edges_path).unwrap_or(0);
let rules_path = args.rules.unwrap_or_else(|| kg_dir.join("rules.toml"));
let mut rule_results: Vec<RuleResult> = structural_checks(&entities_path, &edges_path);
if !args.no_rules && rules_path.exists() {
let configurable = configurable_rule_checks(&entities_path, &edges_path, &rules_path)?;
rule_results.extend(configurable);
}
let errors: usize = rule_results
.iter()
.filter(|r| r.severity == "error" && !r.passed)
.count();
let warnings: usize = rule_results
.iter()
.filter(|r| r.severity == "warning" && !r.passed)
.count();
let info: usize = rule_results
.iter()
.filter(|r| r.severity == "info" && !r.passed)
.count();
let passed = if args.strict {
errors == 0 && warnings == 0
} else {
errors == 0
};
let summary = ValidationSummary {
errors,
warnings,
info,
entities,
edges,
passed,
};
let report = ValidationReport {
rules: rule_results,
summary,
};
match args.format {
OutputFormat::Json => {
let json = serde_json::to_string_pretty(&report).expect("serialize ValidationReport");
println!("{json}");
}
OutputFormat::Github => print_github_format(&report),
OutputFormat::Text => print_text_format(&report, args.verbose, args.quiet),
}
if args.fix {
apply_fixes(&args.repo)?;
}
if !report.summary.passed {
std::process::exit(1);
}
Ok(())
}
fn count_ndjson_lines(path: &Path) -> Option<usize> {
let content = std::fs::read_to_string(path).ok()?;
Some(content.lines().filter(|l| !l.trim().is_empty()).count())
}
fn structural_checks(entities_path: &Path, edges_path: &Path) -> Vec<RuleResult> {
vec![
check_no_duplicate_uuids(entities_path),
check_sort_order(entities_path, edges_path),
check_referential_integrity(entities_path, edges_path),
]
}
fn check_no_duplicate_uuids(entities_path: &Path) -> RuleResult {
let mut seen = std::collections::HashSet::new();
let mut violations = Vec::new();
if let Ok(content) = std::fs::read_to_string(entities_path) {
for line in content.lines().filter(|l| !l.trim().is_empty()) {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(line) {
if let Some(id) = v.get("id").and_then(|i| i.as_str()) {
if !seen.insert(id.to_string()) {
violations.push(Violation {
entity_id: Some(id.to_string()),
entity_name: v.get("name").and_then(|n| n.as_str()).map(str::to_string),
entity_kind: v.get("kind").and_then(|k| k.as_str()).map(str::to_string),
rule_id: "no-duplicate-uuids".into(),
severity: "error",
message: format!("Duplicate UUID: {id}"),
fixable: false,
});
}
}
}
}
}
RuleResult {
id: "no-duplicate-uuids".into(),
severity: "error",
passed: violations.is_empty(),
violations,
}
}
fn check_sort_order(entities_path: &Path, edges_path: &Path) -> RuleResult {
let mut violations = Vec::new();
if let Ok(content) = std::fs::read_to_string(entities_path) {
let ids: Vec<String> = content
.lines()
.filter(|l| !l.trim().is_empty())
.filter_map(|l| {
serde_json::from_str::<serde_json::Value>(l)
.ok()
.and_then(|v| v.get("id")?.as_str().map(str::to_string))
})
.collect();
let mut sorted = ids.clone();
sorted.sort();
if ids != sorted {
violations.push(Violation {
entity_id: None,
entity_name: None,
entity_kind: None,
rule_id: "sort-order".into(),
severity: "warning",
message: "entities.ndjson is not sorted by UUID; run `kkernel kg validate --fix`"
.into(),
fixable: true,
});
}
}
if let Ok(content) = std::fs::read_to_string(edges_path) {
let keys: Vec<(String, String, String)> = content
.lines()
.filter(|l| !l.trim().is_empty())
.filter_map(|l| {
let v: serde_json::Value = serde_json::from_str(l).ok()?;
let s = v.get("source_id")?.as_str()?.to_string();
let t = v.get("target_id")?.as_str()?.to_string();
let r = v.get("relation")?.as_str()?.to_string();
Some((s, t, r))
})
.collect();
let mut sorted = keys.clone();
sorted.sort();
if keys != sorted {
violations.push(Violation {
entity_id: None,
entity_name: None,
entity_kind: None,
rule_id: "sort-order".into(),
severity: "warning",
message:
"edges.ndjson is not sorted by (source, target, relation); run `kkernel kg validate --fix`"
.into(),
fixable: true,
});
}
}
RuleResult {
id: "sort-order".into(),
severity: "warning",
passed: violations.is_empty(),
violations,
}
}
fn check_referential_integrity(entities_path: &Path, edges_path: &Path) -> RuleResult {
let mut violations = Vec::new();
let entity_ids: std::collections::HashSet<String> =
if let Ok(content) = std::fs::read_to_string(entities_path) {
content
.lines()
.filter(|l| !l.trim().is_empty())
.filter_map(|l| {
serde_json::from_str::<serde_json::Value>(l)
.ok()
.and_then(|v| v.get("id")?.as_str().map(str::to_string))
})
.collect()
} else {
std::collections::HashSet::new()
};
if let Ok(content) = std::fs::read_to_string(edges_path) {
for line in content.lines().filter(|l| !l.trim().is_empty()) {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(line) {
for field in &["source_id", "target_id"] {
if let Some(id) = v.get(field).and_then(|i| i.as_str()) {
if !entity_ids.contains(id) {
violations.push(Violation {
entity_id: Some(id.to_string()),
entity_name: None,
entity_kind: None,
rule_id: "referential-integrity".into(),
severity: "error",
message: format!(
"Edge {} references unknown entity: {id}",
if *field == "source_id" {
"source"
} else {
"target"
}
),
fixable: false,
});
}
}
}
}
}
}
RuleResult {
id: "referential-integrity".into(),
severity: "error",
passed: violations.is_empty(),
violations,
}
}
#[derive(Debug, Deserialize)]
struct RuleConfig {
id: String,
#[serde(default = "default_severity")]
severity: String,
kind: String,
condition: Option<String>,
require_field: Option<String>,
#[serde(default)]
message: String,
}
fn default_severity() -> String {
"warning".to_owned()
}
#[derive(Debug, Deserialize)]
struct RulesFile {
#[serde(default)]
rules: Vec<RuleConfig>,
}
fn severity_static(s: &str) -> &'static str {
match s {
"error" => "error",
"info" => "info",
_ => "warning",
}
}
fn validate_severity(rule_id: &str, s: &str) -> Option<RuleResult> {
match s {
"error" | "warning" | "info" => None,
other => Some(RuleResult {
id: rule_id.to_string(),
severity: "error",
passed: false,
violations: vec![Violation {
entity_id: None,
entity_name: None,
entity_kind: None,
rule_id: rule_id.to_string(),
severity: "error",
message: format!(
"Rule {rule_id:?}: invalid severity {other:?}; \
must be \"error\", \"warning\", or \"info\""
),
fixable: false,
}],
}),
}
}
pub(super) fn configurable_rule_checks(
entities_path: &Path,
edges_path: &Path,
rules_path: &Path,
) -> Result<Vec<RuleResult>> {
let ext = rules_path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
if matches!(ext, "yaml" | "yml") {
bail!(
"rules file {:?} uses YAML format which is not supported in this build. \
Rename it to {}.toml and use TOML format instead.",
rules_path,
rules_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("rules")
);
}
let content = std::fs::read_to_string(rules_path)
.with_context(|| format!("read rules file {}", rules_path.display()))?;
let rules_file: RulesFile = toml::from_str(&content)
.with_context(|| format!("parse rules TOML {}", rules_path.display()))?;
let mut results = Vec::with_capacity(rules_file.rules.len());
for rule in &rules_file.rules {
if let Some(err_result) = validate_severity(&rule.id, &rule.severity) {
results.push(err_result);
continue;
}
let path = match rule.kind.as_str() {
"entity" => entities_path,
"edge" => edges_path,
other => {
results.push(RuleResult {
id: rule.id.clone(),
severity: "error",
passed: false,
violations: vec![Violation {
entity_id: None,
entity_name: None,
entity_kind: None,
rule_id: rule.id.clone(),
severity: "error",
message: format!(
"Rule {:?}: unknown kind {other:?}; must be \"entity\" or \"edge\"",
rule.id
),
fixable: false,
}],
});
continue;
}
};
let violations = evaluate_rule(rule, path);
let sev = severity_static(&rule.severity);
results.push(RuleResult {
id: rule.id.clone(),
severity: sev,
passed: violations.is_empty(),
violations,
});
}
Ok(results)
}
fn evaluate_rule(rule: &RuleConfig, path: &Path) -> Vec<Violation> {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return vec![],
};
let condition: Option<(&str, &str)> = rule.condition.as_deref().and_then(|c| c.split_once('='));
let sev = severity_static(&rule.severity);
let mut violations = Vec::new();
for line in content.lines().filter(|l| !l.trim().is_empty()) {
let v: serde_json::Value = match serde_json::from_str(line) {
Ok(val) => val,
Err(_) => continue,
};
if let Some((field, expected)) = condition {
if field == "source_id" && expected == "target_id" {
let src = v.get("source_id").and_then(|s| s.as_str()).unwrap_or("");
let tgt = v.get("target_id").and_then(|s| s.as_str()).unwrap_or("");
if src == tgt {
violations.push(Violation {
entity_id: Some(src.to_owned()),
entity_name: None,
entity_kind: v
.get("relation")
.and_then(|r| r.as_str())
.map(str::to_owned),
rule_id: rule.id.clone(),
severity: sev,
message: rule.message.replace("{id}", src),
fixable: false,
});
}
continue;
}
let actual = v.get(field).and_then(|f| f.as_str()).unwrap_or("");
if actual != expected {
continue;
}
}
if let Some(req) = rule.require_field.as_deref() {
let val = v.get(req).and_then(|f| f.as_str()).unwrap_or("");
if val.is_empty() {
let id = v.get("id").and_then(|i| i.as_str()).unwrap_or("");
violations.push(Violation {
entity_id: if id.is_empty() {
None
} else {
Some(id.to_owned())
},
entity_name: v.get("name").and_then(|n| n.as_str()).map(str::to_owned),
entity_kind: v.get("kind").and_then(|k| k.as_str()).map(str::to_owned),
rule_id: rule.id.clone(),
severity: sev,
message: rule.message.replace("{id}", id),
fixable: false,
});
}
}
}
violations
}
fn apply_fixes(repo: &std::path::Path) -> Result<()> {
let kg_dir = repo.join(".khive/kg");
fix_sort_order(&kg_dir.join("entities.ndjson"), "id")?;
fix_sort_order_edges(&kg_dir.join("edges.ndjson"))?;
eprintln!("~ sort-order: applied fix to entities.ndjson and edges.ndjson");
Ok(())
}
pub(super) fn fix_sort_order(path: &Path, sort_key: &str) -> Result<()> {
if !path.exists() {
return Ok(());
}
let content =
std::fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
let mut lines: Vec<serde_json::Value> = content
.lines()
.filter(|l| !l.trim().is_empty())
.filter_map(|l| serde_json::from_str(l).ok())
.collect();
lines.sort_by(|a, b| {
let ak = a.get(sort_key).and_then(|v| v.as_str()).unwrap_or("");
let bk = b.get(sort_key).and_then(|v| v.as_str()).unwrap_or("");
ak.cmp(bk)
});
let out: String = lines
.iter()
.map(|v| serde_json::to_string(v).unwrap())
.collect::<Vec<_>>()
.join("\n");
std::fs::write(path, out + "\n").with_context(|| format!("write {}", path.display()))
}
fn fix_sort_order_edges(path: &Path) -> Result<()> {
if !path.exists() {
return Ok(());
}
let content =
std::fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
let mut lines: Vec<serde_json::Value> = content
.lines()
.filter(|l| !l.trim().is_empty())
.filter_map(|l| serde_json::from_str(l).ok())
.collect();
lines.sort_by(|a, b| {
let ak = (
a.get("source_id").and_then(|v| v.as_str()).unwrap_or(""),
a.get("target_id").and_then(|v| v.as_str()).unwrap_or(""),
a.get("relation").and_then(|v| v.as_str()).unwrap_or(""),
);
let bk = (
b.get("source_id").and_then(|v| v.as_str()).unwrap_or(""),
b.get("target_id").and_then(|v| v.as_str()).unwrap_or(""),
b.get("relation").and_then(|v| v.as_str()).unwrap_or(""),
);
ak.cmp(&bk)
});
let out: String = lines
.iter()
.map(|v| serde_json::to_string(v).unwrap())
.collect::<Vec<_>>()
.join("\n");
std::fs::write(path, out + "\n").with_context(|| format!("write {}", path.display()))
}
fn print_text_format(report: &ValidationReport, verbose: bool, quiet: bool) {
if !quiet {
for r in &report.rules {
let symbol = if r.passed {
"\u{2713}"
} else if r.severity == "error" {
"\u{2717}"
} else {
"\u{26a0}"
};
if r.violations.is_empty() {
println!(" {symbol} {}", r.id);
} else {
println!(" {symbol} {}: {} violation(s)", r.id, r.violations.len());
let shown = if verbose {
r.violations.len()
} else {
2.min(r.violations.len())
};
for v in &r.violations[..shown] {
println!(" - {}", v.message);
}
if !verbose && r.violations.len() > 2 {
println!(" + {} more (run with --verbose)", r.violations.len() - 2);
}
}
}
}
let s = &report.summary;
println!(
"\nSummary: {} error(s), {} warning(s), {} entities, {} edges",
s.errors, s.warnings, s.entities, s.edges
);
}
fn print_github_format(report: &ValidationReport) {
for r in &report.rules {
for v in &r.violations {
let level = if r.severity == "error" {
"error"
} else {
"warning"
};
println!("::{level} ::{}", v.message);
}
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use tempfile::TempDir;
use super::*;
fn make_kg_dir(tmp: &TempDir) -> PathBuf {
let kg_dir = tmp.path().join(".khive/kg");
std::fs::create_dir_all(&kg_dir).unwrap();
kg_dir
}
fn write_entities(kg_dir: &std::path::Path, entities: &[(&str, &str, &str)]) {
let content: String = entities
.iter()
.map(|(id, kind, name)| format!(r#"{{"id":"{id}","kind":"{kind}","name":"{name}"}}"#))
.collect::<Vec<_>>()
.join("\n");
std::fs::write(kg_dir.join("entities.ndjson"), content + "\n").unwrap();
}
fn write_edges(kg_dir: &std::path::Path, edges: &[(&str, &str, &str)]) {
let content: String = edges
.iter()
.map(|(src, tgt, rel)| {
format!(r#"{{"source_id":"{src}","target_id":"{tgt}","relation":"{rel}"}}"#)
})
.collect::<Vec<_>>()
.join("\n");
std::fs::write(kg_dir.join("edges.ndjson"), content + "\n").unwrap();
}
#[test]
fn duplicate_uuid_detected() {
let tmp = TempDir::new().unwrap();
let kg_dir = make_kg_dir(&tmp);
write_entities(
&kg_dir,
&[
("aaaaaaaa-0000-0000-0000-000000000001", "concept", "A"),
("aaaaaaaa-0000-0000-0000-000000000001", "concept", "A-dup"),
],
);
let result = check_no_duplicate_uuids(&kg_dir.join("entities.ndjson"));
assert!(!result.passed, "duplicate UUID should fail");
assert_eq!(result.violations.len(), 1);
}
#[test]
fn no_duplicates_passes() {
let tmp = TempDir::new().unwrap();
let kg_dir = make_kg_dir(&tmp);
write_entities(
&kg_dir,
&[
("aaaaaaaa-0000-0000-0000-000000000001", "concept", "A"),
("bbbbbbbb-0000-0000-0000-000000000002", "concept", "B"),
],
);
let result = check_no_duplicate_uuids(&kg_dir.join("entities.ndjson"));
assert!(result.passed);
}
#[test]
fn referential_integrity_catches_missing_target() {
let tmp = TempDir::new().unwrap();
let kg_dir = make_kg_dir(&tmp);
write_entities(
&kg_dir,
&[("aaaaaaaa-0000-0000-0000-000000000001", "concept", "A")],
);
write_edges(
&kg_dir,
&[(
"aaaaaaaa-0000-0000-0000-000000000001",
"bbbbbbbb-0000-0000-0000-000000000002",
"extends",
)],
);
let result = check_referential_integrity(
&kg_dir.join("entities.ndjson"),
&kg_dir.join("edges.ndjson"),
);
assert!(!result.passed);
assert_eq!(result.violations.len(), 1);
}
#[test]
fn configurable_rule_checks_empty_rules_file_returns_no_results() {
let tmp = TempDir::new().unwrap();
let kg_dir = make_kg_dir(&tmp);
write_entities(
&kg_dir,
&[("aaaaaaaa-0000-0000-0000-000000000001", "concept", "A")],
);
std::fs::write(kg_dir.join("edges.ndjson"), "").unwrap();
let rules_path = tmp.path().join("rules.toml");
std::fs::write(&rules_path, "rules = []\n").unwrap();
let results = configurable_rule_checks(
&kg_dir.join("entities.ndjson"),
&kg_dir.join("edges.ndjson"),
&rules_path,
)
.unwrap();
assert!(results.is_empty(), "no rules → no results");
}
#[test]
fn configurable_rule_checks_require_field_detects_missing_description() {
let tmp = TempDir::new().unwrap();
let kg_dir = make_kg_dir(&tmp);
let entities = r#"{"id":"aaa1","kind":"concept","name":"A","description":"has one"}
{"id":"aaa2","kind":"concept","name":"B"}
"#;
std::fs::write(kg_dir.join("entities.ndjson"), entities).unwrap();
std::fs::write(kg_dir.join("edges.ndjson"), "").unwrap();
let rules_toml = r#"
[[rules]]
id = "concept-must-have-description"
severity = "warning"
kind = "entity"
condition = "kind=concept"
require_field = "description"
message = "Concept {id} missing description"
"#;
let rules_path = tmp.path().join("rules.toml");
std::fs::write(&rules_path, rules_toml).unwrap();
let results = configurable_rule_checks(
&kg_dir.join("entities.ndjson"),
&kg_dir.join("edges.ndjson"),
&rules_path,
)
.unwrap();
assert_eq!(results.len(), 1);
let r = &results[0];
assert_eq!(r.id, "concept-must-have-description");
assert!(
!r.passed,
"rule should fail when a concept lacks description"
);
assert_eq!(r.violations.len(), 1);
assert_eq!(r.violations[0].entity_id.as_deref(), Some("aaa2"));
}
#[test]
fn configurable_rule_checks_self_loop_sentinel_detects_loop() {
let tmp = TempDir::new().unwrap();
let kg_dir = make_kg_dir(&tmp);
write_entities(
&kg_dir,
&[
("aaaaaaaa-0000-0000-0000-000000000001", "concept", "A"),
("bbbbbbbb-0000-0000-0000-000000000002", "concept", "B"),
],
);
let edges = r#"{"source_id":"aaaaaaaa-0000-0000-0000-000000000001","target_id":"aaaaaaaa-0000-0000-0000-000000000001","relation":"extends"}
{"source_id":"aaaaaaaa-0000-0000-0000-000000000001","target_id":"bbbbbbbb-0000-0000-0000-000000000002","relation":"extends"}
"#;
std::fs::write(kg_dir.join("edges.ndjson"), edges).unwrap();
let rules_toml = r#"
[[rules]]
id = "no-self-loops"
severity = "error"
kind = "edge"
condition = "source_id=target_id"
message = "Self-loop detected on {id}"
"#;
let rules_path = tmp.path().join("rules.toml");
std::fs::write(&rules_path, rules_toml).unwrap();
let results = configurable_rule_checks(
&kg_dir.join("entities.ndjson"),
&kg_dir.join("edges.ndjson"),
&rules_path,
)
.unwrap();
assert_eq!(results.len(), 1);
let r = &results[0];
assert!(!r.passed);
assert_eq!(r.violations.len(), 1, "exactly one self-loop");
}
#[test]
fn configurable_rule_checks_yaml_extension_returns_error() {
let tmp = TempDir::new().unwrap();
let kg_dir = make_kg_dir(&tmp);
write_entities(
&kg_dir,
&[("aaaaaaaa-0000-0000-0000-000000000001", "concept", "A")],
);
std::fs::write(kg_dir.join("edges.ndjson"), "").unwrap();
let rules_path = tmp.path().join("rules.yaml");
std::fs::write(&rules_path, "rules: []\n").unwrap();
let result = configurable_rule_checks(
&kg_dir.join("entities.ndjson"),
&kg_dir.join("edges.ndjson"),
&rules_path,
);
assert!(result.is_err(), "YAML extension must return an error");
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("YAML") || msg.contains("toml"),
"error message should mention TOML: {msg}"
);
}
#[test]
fn configurable_rule_checks_unknown_kind_produces_error_result() {
let tmp = TempDir::new().unwrap();
let kg_dir = make_kg_dir(&tmp);
write_entities(
&kg_dir,
&[("aaaaaaaa-0000-0000-0000-000000000001", "concept", "A")],
);
std::fs::write(kg_dir.join("edges.ndjson"), "").unwrap();
let rules_toml = r#"
[[rules]]
id = "bad-kind"
severity = "error"
kind = "note"
condition = "kind=concept"
require_field = "description"
message = "bad"
"#;
let rules_path = tmp.path().join("rules.toml");
std::fs::write(&rules_path, rules_toml).unwrap();
let results = configurable_rule_checks(
&kg_dir.join("entities.ndjson"),
&kg_dir.join("edges.ndjson"),
&rules_path,
)
.unwrap();
assert_eq!(results.len(), 1);
assert!(!results[0].passed);
assert_eq!(results[0].severity, "error");
}
#[test]
fn configurable_rule_checks_invalid_severity_produces_error_result() {
let tmp = TempDir::new().unwrap();
let kg_dir = make_kg_dir(&tmp);
write_entities(
&kg_dir,
&[("aaaaaaaa-0000-0000-0000-000000000001", "concept", "A")],
);
std::fs::write(kg_dir.join("edges.ndjson"), "").unwrap();
let rules_toml = r#"
[[rules]]
id = "bad-severity"
severity = "erorr"
kind = "entity"
require_field = "description"
message = "bad"
"#;
let rules_path = tmp.path().join("rules.toml");
std::fs::write(&rules_path, rules_toml).unwrap();
let results = configurable_rule_checks(
&kg_dir.join("entities.ndjson"),
&kg_dir.join("edges.ndjson"),
&rules_path,
)
.unwrap();
assert_eq!(results.len(), 1);
assert!(!results[0].passed, "invalid severity must fail");
assert_eq!(results[0].severity, "error");
assert!(
results[0].violations[0]
.message
.contains("invalid severity"),
"error message should mention invalid severity"
);
}
#[test]
fn sort_order_fix_sorts_entities() {
let tmp = TempDir::new().unwrap();
let kg_dir = make_kg_dir(&tmp);
write_entities(
&kg_dir,
&[
("cccccccc-0000-0000-0000-000000000003", "concept", "C"),
("aaaaaaaa-0000-0000-0000-000000000001", "concept", "A"),
("bbbbbbbb-0000-0000-0000-000000000002", "concept", "B"),
],
);
std::fs::write(kg_dir.join("edges.ndjson"), "").unwrap();
fix_sort_order(&kg_dir.join("entities.ndjson"), "id").unwrap();
let result = check_sort_order(
&kg_dir.join("entities.ndjson"),
&kg_dir.join("edges.ndjson"),
);
assert!(result.passed, "sort-order should pass after fix");
}
}