Unify atoms and rules in kei-sage's graph. Previously [[rules/...]] wikilinks were filtered (explicit Stream C scope-deferral). Now they resolve to rule-node units with rule_ref edges. kei-atom-discovery extension (non-breaking): - WikilinkTarget enum: Atom(String) | Rule(String) | Other(String) - classify_wikilink(inner: &str) -> WikilinkTarget — exposed via lib.rs - parse_wikilink unchanged for backwards-compat; new callers use classify for richer semantics kei-sage additions: - rule_index.rs (129 LOC) — RuleRecord + discover_rules walking flat *.md + extract_h1 for display name + index_rules (unit_type="rule", vault_path="rule:<slug>") + index_rule_edges (walks atom.related, emits rule_ref edges atom → rule node) - atom_cli.rs: cmd_rules_discover + default_rules_root - main.rs: AtomsRulesDiscover subcommand with --rules-root flag - tests/rules_smoke.rs: 5 tests (discovery, heading extraction, slug fallback for headingless files, empty-dir, atom→rule edge persistence) Tests: 12/12 kei-atom-discovery (+3 classify_wikilink), 28/28 kei-sage (+5 rules_smoke + unit tests now counted). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
132 lines
4.1 KiB
Rust
132 lines
4.1 KiB
Rust
//! Integration smoke test for rule discovery + atom→rule edge persistence.
|
|
//!
|
|
//! Creates a temp rules tree with 2 rule files (flat dir), asserts
|
|
//! `discover_rules` extracts slugs + heading names correctly. Then stages
|
|
//! an atom whose `related:` lists one of those rules and asserts
|
|
//! `index_rule_edges` persists a `rule_ref` edge into the store.
|
|
|
|
use kei_sage::atoms::discover_atoms;
|
|
use kei_sage::edges::list_outgoing;
|
|
use kei_sage::rule_index::{discover_rules, index_rule_edges, index_rules};
|
|
use kei_sage::Store;
|
|
use std::fs;
|
|
use tempfile::tempdir;
|
|
|
|
const RULE_012: &str = r#"# RULE 0.12 — AGENT GIT MODEL
|
|
|
|
Body of the rule.
|
|
"#;
|
|
|
|
const RULE_MEMORY: &str = r#"# Memory Protocol
|
|
|
|
3-layer architecture.
|
|
"#;
|
|
|
|
const ATOM_A: &str = r#"---
|
|
atom: kei-task::create
|
|
kind: command
|
|
version: "0.1.0"
|
|
input:
|
|
schema: schemas/create-input.json
|
|
output:
|
|
schema: schemas/create-output.json
|
|
stability: stable
|
|
keywords: [task]
|
|
related:
|
|
- "[[rules/RULE 0.12]]"
|
|
- "[[rules/memory-protocol]]"
|
|
---
|
|
# kei-task::create
|
|
|
|
Body.
|
|
"#;
|
|
|
|
fn write_rule(root: &std::path::Path, slug: &str, body: &str) {
|
|
fs::create_dir_all(root).unwrap();
|
|
fs::write(root.join(format!("{slug}.md")), body).unwrap();
|
|
}
|
|
|
|
fn write_atom(root: &std::path::Path, crate_name: &str, verb: &str, body: &str) {
|
|
let atoms_dir = root.join(crate_name).join("atoms");
|
|
fs::create_dir_all(&atoms_dir).unwrap();
|
|
fs::write(atoms_dir.join(format!("{verb}.md")), body).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn discover_rules_returns_two_records_with_correct_slugs_and_names() {
|
|
let tmp = tempdir().unwrap();
|
|
write_rule(tmp.path(), "agent-git-model", RULE_012);
|
|
write_rule(tmp.path(), "memory-protocol", RULE_MEMORY);
|
|
|
|
let recs = discover_rules(tmp.path()).unwrap();
|
|
assert_eq!(recs.len(), 2, "expected 2 rules, got {}", recs.len());
|
|
|
|
let by_slug: std::collections::HashMap<_, _> =
|
|
recs.iter().map(|r| (r.slug.as_str(), r.name.as_str())).collect();
|
|
assert_eq!(
|
|
by_slug.get("agent-git-model"),
|
|
Some(&"RULE 0.12 — AGENT GIT MODEL")
|
|
);
|
|
assert_eq!(by_slug.get("memory-protocol"), Some(&"Memory Protocol"));
|
|
}
|
|
|
|
#[test]
|
|
fn index_rules_persists_rule_units() {
|
|
let tmp = tempdir().unwrap();
|
|
write_rule(tmp.path(), "memory-protocol", RULE_MEMORY);
|
|
|
|
let recs = discover_rules(tmp.path()).unwrap();
|
|
let store = Store::open_memory().unwrap();
|
|
let n = index_rules(&store, &recs).unwrap();
|
|
assert_eq!(n, 1);
|
|
assert_eq!(store.count_units().unwrap(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn index_rule_edges_persists_atom_to_rule() {
|
|
let tmp_rules = tempdir().unwrap();
|
|
let tmp_atoms = tempdir().unwrap();
|
|
|
|
// 1 rule file; the atom references `rules/RULE 0.12` → slug "0.12".
|
|
write_rule(tmp_rules.path(), "agent-git-model", RULE_012);
|
|
write_atom(tmp_atoms.path(), "kei-task", "create", ATOM_A);
|
|
|
|
let rule_recs = discover_rules(tmp_rules.path()).unwrap();
|
|
let atom_recs = discover_atoms(tmp_atoms.path()).unwrap();
|
|
|
|
let store = Store::open_memory().unwrap();
|
|
index_rules(&store, &rule_recs).unwrap();
|
|
let edges_written = index_rule_edges(&store, &atom_recs).unwrap();
|
|
|
|
// 2 rule wikilinks in ATOM_A — "rules/RULE 0.12" → "0.12" and
|
|
// "rules/memory-protocol" → "memory-protocol". Both edges persisted
|
|
// regardless of whether the rule unit exists (edges are path-keyed).
|
|
assert_eq!(edges_written, 2);
|
|
|
|
let outgoing = list_outgoing(&store, "kei-task::create").unwrap();
|
|
let rule_edges: Vec<&str> = outgoing
|
|
.iter()
|
|
.filter(|e| e.edge_type == "rule_ref")
|
|
.map(|e| e.dst_path.as_str())
|
|
.collect();
|
|
assert!(rule_edges.contains(&"rule:0.12"));
|
|
assert!(rule_edges.contains(&"rule:memory-protocol"));
|
|
}
|
|
|
|
#[test]
|
|
fn discover_rules_empty_dir_returns_empty() {
|
|
let tmp = tempdir().unwrap();
|
|
let recs = discover_rules(tmp.path()).unwrap();
|
|
assert!(recs.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn discover_rules_without_heading_falls_back_to_slug() {
|
|
let tmp = tempdir().unwrap();
|
|
fs::write(tmp.path().join("plain.md"), "no heading in this file\n").unwrap();
|
|
|
|
let recs = discover_rules(tmp.path()).unwrap();
|
|
assert_eq!(recs.len(), 1);
|
|
assert_eq!(recs[0].slug, "plain");
|
|
assert_eq!(recs[0].name, "plain");
|
|
}
|