KeiSeiKit-1.0/_primitives/_rust/kei-sage/src/lineage.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

195 lines
6.2 KiB
Rust

//! Lineage traversal for primitive TOMLs.
//!
//! Parses `[lineage]` section of capability.toml + manifest TOMLs,
//! extracting `parents` wikilinks, `created-by`, `fork-from`. Builds
//! an in-memory directed graph and walks ancestors + descendants.
use anyhow::{Context, Result};
use kei_atom_discovery::parse_wikilink;
use serde::Deserialize;
use std::collections::{BTreeMap, HashSet, VecDeque};
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
/// Lineage metadata for a single primitive.
#[derive(Debug, Clone)]
pub struct LineageNode {
pub id: String,
pub source: PathBuf,
pub parents: Vec<String>,
pub created_by: Option<String>,
pub fork_from: Option<String>,
pub created_at: Option<String>,
}
#[derive(Debug, Deserialize)]
struct CapDoc {
capability: Option<IdHead>,
#[serde(default)]
lineage: Option<LineageSection>,
}
#[derive(Debug, Deserialize)]
struct ManDoc {
name: Option<String>,
#[serde(default)]
lineage: Option<LineageSection>,
}
#[derive(Debug, Deserialize)]
struct IdHead {
name: Option<String>,
}
#[derive(Debug, Deserialize, Default)]
struct LineageSection {
#[serde(default)]
parents: Vec<String>,
#[serde(rename = "created-by", default)]
created_by: Option<String>,
#[serde(rename = "fork-from", default)]
fork_from: Option<String>,
#[serde(rename = "created-at", default)]
created_at: Option<String>,
}
/// Parse a single TOML into a `LineageNode`, or `None` if unidentifiable.
pub fn parse_lineage(path: &Path) -> Result<Option<LineageNode>> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("read {}", path.display()))?;
if let Some(n) = parse_cap_lineage(&text, path) {
return Ok(Some(n));
}
Ok(parse_man_lineage(&text, path))
}
fn parse_cap_lineage(text: &str, path: &Path) -> Option<LineageNode> {
let doc: CapDoc = toml::from_str(text).ok()?;
let id = doc.capability.as_ref().and_then(|c| c.name.clone())?;
Some(build_node(id, path, doc.lineage))
}
fn parse_man_lineage(text: &str, path: &Path) -> Option<LineageNode> {
let doc: ManDoc = toml::from_str(text).ok()?;
let id = doc.name?;
Some(build_node(id, path, doc.lineage))
}
fn build_node(id: String, path: &Path, lin: Option<LineageSection>) -> LineageNode {
let lin = lin.unwrap_or_default();
let parents = lin.parents.iter().filter_map(|w| parse_wikilink(w)).collect();
LineageNode {
id,
source: path.to_path_buf(),
parents,
created_by: lin.created_by,
fork_from: lin.fork_from,
created_at: lin.created_at,
}
}
/// Walk capabilities + manifests roots and parse every lineage node.
pub fn discover_lineage(cap_root: &Path, man_root: &Path) -> Vec<LineageNode> {
let mut out = Vec::new();
walk_root(cap_root, "capability.toml", 4, &mut out);
walk_manifest_root(man_root, &mut out);
out
}
fn walk_root(root: &Path, fname: &str, depth: usize, out: &mut Vec<LineageNode>) {
if !root.is_dir() {
return;
}
for e in WalkDir::new(root).max_depth(depth).follow_links(false).into_iter().flatten() {
if e.file_name() == fname && e.path().is_file() {
if let Ok(Some(n)) = parse_lineage(e.path()) {
out.push(n);
}
}
}
}
fn walk_manifest_root(root: &Path, out: &mut Vec<LineageNode>) {
if !root.is_dir() {
return;
}
for e in WalkDir::new(root).max_depth(2).follow_links(false).into_iter().flatten() {
let p = e.path();
if p.is_file() && p.extension().and_then(|s| s.to_str()) == Some("toml") {
if let Ok(Some(n)) = parse_lineage(p) {
out.push(n);
}
}
}
}
/// Traversal result: ancestors (via parents + fork-from) and descendants.
#[derive(Debug, Clone, Default)]
pub struct LineageTrace {
pub focus: Option<LineageNode>,
pub ancestors: Vec<String>,
pub descendants: Vec<String>,
}
/// BFS ancestors (follow parents + fork_from) + descendants (inverse edges).
pub fn trace_lineage(nodes: &[LineageNode], id: &str, depth: usize) -> LineageTrace {
let by_id: BTreeMap<&str, &LineageNode> = nodes.iter().map(|n| (n.id.as_str(), n)).collect();
LineageTrace {
focus: by_id.get(id).map(|n| (*n).clone()),
ancestors: bfs_up(&by_id, id, depth),
descendants: bfs_down(nodes, id, depth),
}
}
fn bfs_up(by_id: &BTreeMap<&str, &LineageNode>, start: &str, depth: usize) -> Vec<String> {
let mut seen: HashSet<String> = HashSet::new();
let mut queue: VecDeque<(String, usize)> = VecDeque::new();
queue.push_back((start.to_string(), 0));
let mut out = Vec::new();
while let Some((cur, d)) = queue.pop_front() {
if d >= depth { continue; }
let Some(n) = by_id.get(cur.as_str()) else { continue };
let mut parents = n.parents.clone();
if let Some(f) = &n.fork_from { parents.push(f.clone()); }
for p in parents {
if seen.insert(p.clone()) {
out.push(p.clone());
queue.push_back((p, d + 1));
}
}
}
out
}
fn bfs_down(nodes: &[LineageNode], start: &str, depth: usize) -> Vec<String> {
let mut seen: HashSet<String> = HashSet::new();
let mut frontier: Vec<String> = vec![start.to_string()];
let mut out = Vec::new();
for _ in 0..depth {
let mut next: Vec<String> = Vec::new();
for n in nodes {
let is_child = n.parents.iter().any(|p| frontier.contains(p))
|| n.fork_from.as_ref().is_some_and(|f| frontier.contains(f));
if is_child && seen.insert(n.id.clone()) {
out.push(n.id.clone());
next.push(n.id.clone());
}
}
if next.is_empty() { break; }
frontier = next;
}
out
}
/// Filter + sort nodes by a creator id, return most-recent first (by created_at).
pub fn nodes_by_author(nodes: &[LineageNode], creator: &str, limit: usize) -> Vec<LineageNode> {
let mut matched: Vec<LineageNode> = nodes
.iter()
.filter(|n| n.created_by.as_deref() == Some(creator))
.cloned()
.collect();
matched.sort_by(|a, b| b.created_at.cmp(&a.created_at));
matched.truncate(limit);
matched
}