KeiSeiKit-1.0/_primitives/_rust/kei-sage/src/rule_index.rs
Parfii-bot 0be354a920 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

129 lines
3.9 KiB
Rust

//! Rule discovery + indexing for kei-sage.
//!
//! Walks a flat `<rules-root>/*.md` tree (e.g. `~/.claude/rules/`), extracts
//! the first `#` heading from each file as the rule name, and uses the file
//! stem as the rule slug. Rules are persisted as Units with:
//! - `unit_type = "rule"`
//! - `vault_path = "rule:<slug>"`
//! - `title = <heading>`
//!
//! Edges from atoms that `related:` a `[[rules/...]]` wikilink are persisted
//! with `edge_type = "rule_ref"` via `index_rule_edges`.
//!
//! Scope: flat dir only (rules live flat in `~/.claude/rules/`). No recursion.
use crate::atoms::AtomRecord;
use crate::edges::add_edge;
use crate::store::Store;
use crate::types::Unit;
use anyhow::Result;
use kei_atom_discovery::{classify_wikilink, parse_wikilink, WikilinkTarget};
use std::fs;
use std::path::{Path, PathBuf};
/// One discovered rule: slug (file stem), display name (`# heading`), md path.
#[derive(Debug, Clone)]
pub struct RuleRecord {
pub slug: String,
pub name: String,
pub md_path: PathBuf,
}
/// Walk `<root>/*.md` (no recursion) and parse each file's first `#` heading.
/// Files without a heading fall back to the file stem as the display name.
pub fn discover_rules(root: &Path) -> Result<Vec<RuleRecord>> {
if !root.is_dir() {
return Ok(Vec::new());
}
let mut out = Vec::new();
for entry in fs::read_dir(root)? {
let entry = entry?;
let path = entry.path();
if !is_rule_md(&path) {
continue;
}
if let Some(rec) = parse_rule_file(&path) {
out.push(rec);
}
}
out.sort_by(|a, b| a.slug.cmp(&b.slug));
Ok(out)
}
fn is_rule_md(path: &Path) -> bool {
path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("md")
}
fn parse_rule_file(path: &Path) -> Option<RuleRecord> {
let slug = path.file_stem().and_then(|s| s.to_str())?.to_string();
let text = fs::read_to_string(path).ok()?;
let name = extract_h1(&text).unwrap_or_else(|| slug.clone());
Some(RuleRecord {
slug,
name,
md_path: path.to_path_buf(),
})
}
/// Extract the first `# ` heading line, stripping the `#` prefix and trim.
/// Returns `None` if no `# ` line exists in the file.
fn extract_h1(text: &str) -> Option<String> {
for line in text.lines() {
let t = line.trim_start();
if let Some(rest) = t.strip_prefix("# ") {
return Some(rest.trim().to_string());
}
}
None
}
/// Persist rule units into the store. Returns the number of units indexed.
pub fn index_rules(store: &Store, records: &[RuleRecord]) -> Result<usize> {
for rec in records {
store.add_unit(&record_to_unit(rec))?;
}
Ok(records.len())
}
fn record_to_unit(rec: &RuleRecord) -> Unit {
Unit {
unit_type: "rule".into(),
title: rec.name.clone(),
content: String::new(),
evidence_grade: "rule".into(),
source_path: rec.md_path.to_string_lossy().into(),
vault_path: format!("rule:{}", rec.slug),
category: "rule".into(),
..Default::default()
}
}
/// Walk every atom's `related:` list. For every wikilink that classifies as
/// `Rule`, persist a `rule_ref` edge from the atom to `rule:<slug>`.
/// Returns the number of edges persisted.
pub fn index_rule_edges(store: &Store, records: &[AtomRecord]) -> Result<usize> {
let mut n = 0;
for rec in records {
for w in &rec.related {
if let Some(slug) = resolve_rule_ref(w) {
add_edge(
store,
&rec.full_id,
&format!("rule:{}", slug),
"rule_ref",
1.0,
)?;
n += 1;
}
}
}
Ok(n)
}
fn resolve_rule_ref(raw: &str) -> Option<String> {
let inner = parse_wikilink(raw)?;
match classify_wikilink(&inner) {
WikilinkTarget::Rule(slug) => Some(slug),
_ => None,
}
}