feat(w10b): kei-sage facet-query walker includes _roles/
Extended discover_primitives to optionally walk _roles/ root (with backward-compat thin wrapper preserving 3 existing facet tests). RoleDoc parser handles [role] name= shape (distinct from [capability] + flat manifests). Role IDs namespaced as role::<name>. CLI: default_roles_root() = ~/.claude/_roles; FacetQuery.roles_root flag optional. Tests: 36/36 (was 34, +2 role-taxonomy + backward-compat). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
78f241dbfc
commit
cd7dc94512
4 changed files with 135 additions and 9 deletions
|
|
@ -6,7 +6,7 @@
|
|||
use crate::atom_index::index_atoms;
|
||||
use crate::atoms::{discover_atoms, AtomRecord};
|
||||
use crate::bfs::bfs;
|
||||
use crate::facet_query::{discover_primitives, matches_all, parse_filters};
|
||||
use crate::facet_query::{discover_primitives_with_roles, matches_all, parse_filters};
|
||||
use crate::lineage::{discover_lineage, nodes_by_author, trace_lineage};
|
||||
use crate::pagerank::pagerank;
|
||||
use crate::rule_index::discover_rules;
|
||||
|
|
@ -35,9 +35,19 @@ pub fn default_manifests_root() -> PathBuf {
|
|||
PathBuf::from(home).join(".claude/_manifests")
|
||||
}
|
||||
|
||||
pub fn cmd_facet_query(cap_root: &Path, man_root: &Path, filters: &[String]) -> Result<()> {
|
||||
pub fn default_roles_root() -> PathBuf {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
|
||||
PathBuf::from(home).join(".claude/_roles")
|
||||
}
|
||||
|
||||
pub fn cmd_facet_query(
|
||||
cap_root: &Path,
|
||||
man_root: &Path,
|
||||
roles_root: &Path,
|
||||
filters: &[String],
|
||||
) -> Result<()> {
|
||||
let pairs = parse_filters(filters);
|
||||
let all = discover_primitives(cap_root, man_root);
|
||||
let all = discover_primitives_with_roles(cap_root, man_root, Some(roles_root));
|
||||
for p in all.iter().filter(|p| matches_all(p, &pairs)) {
|
||||
println!("{}", p.full_id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,14 +38,30 @@ struct ManifestDoc {
|
|||
taxonomy: Option<BTreeMap<String, toml::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RoleDoc {
|
||||
role: Option<RoleHead>,
|
||||
#[serde(default)]
|
||||
taxonomy: Option<BTreeMap<String, toml::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RoleHead {
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
/// Parse a single TOML file into a `PrimitiveFacets`, or `None` if it's
|
||||
/// unparseable or has no discoverable id.
|
||||
/// unparseable or has no discoverable id. Tries capability, then role,
|
||||
/// then flat manifest form.
|
||||
pub fn parse_primitive(path: &Path) -> Result<Option<PrimitiveFacets>> {
|
||||
let text = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("read {}", path.display()))?;
|
||||
if let Some(p) = parse_capability(&text, path) {
|
||||
return Ok(Some(p));
|
||||
}
|
||||
if let Some(p) = parse_role(&text, path) {
|
||||
return Ok(Some(p));
|
||||
}
|
||||
Ok(parse_manifest(&text, path))
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +79,17 @@ fn parse_manifest(text: &str, path: &Path) -> Option<PrimitiveFacets> {
|
|||
Some(PrimitiveFacets { full_id: id, source: path.to_path_buf(), facets })
|
||||
}
|
||||
|
||||
fn parse_role(text: &str, path: &Path) -> Option<PrimitiveFacets> {
|
||||
let doc: RoleDoc = toml::from_str(text).ok()?;
|
||||
let name = doc.role.as_ref().and_then(|r| r.name.clone())?;
|
||||
let facets = flatten_facets(doc.taxonomy.as_ref());
|
||||
Some(PrimitiveFacets {
|
||||
full_id: format!("role::{name}"),
|
||||
source: path.to_path_buf(),
|
||||
facets,
|
||||
})
|
||||
}
|
||||
|
||||
fn flatten_facets(tax: Option<&BTreeMap<String, toml::Value>>) -> BTreeMap<String, String> {
|
||||
let mut out = BTreeMap::new();
|
||||
let Some(map) = tax else { return out };
|
||||
|
|
@ -86,9 +113,22 @@ fn value_to_string(v: &toml::Value) -> Option<String> {
|
|||
/// Walk capabilities + manifests roots and return all parseable primitives.
|
||||
/// Silently skips files that fail to parse (lint is a separate concern).
|
||||
pub fn discover_primitives(cap_root: &Path, man_root: &Path) -> Vec<PrimitiveFacets> {
|
||||
discover_primitives_with_roles(cap_root, man_root, None)
|
||||
}
|
||||
|
||||
/// Same as `discover_primitives`, but also walks an optional roles root
|
||||
/// (`_roles/*.toml`). Role entries emit id `role::<name>`.
|
||||
pub fn discover_primitives_with_roles(
|
||||
cap_root: &Path,
|
||||
man_root: &Path,
|
||||
roles_root: Option<&Path>,
|
||||
) -> Vec<PrimitiveFacets> {
|
||||
let mut out = Vec::new();
|
||||
walk_capabilities(cap_root, &mut out);
|
||||
walk_manifests(man_root, &mut out);
|
||||
if let Some(r) = roles_root {
|
||||
walk_roles(r, &mut out);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
|
|
@ -119,6 +159,20 @@ fn walk_manifests(root: &Path, out: &mut Vec<PrimitiveFacets>) {
|
|||
}
|
||||
}
|
||||
|
||||
fn walk_roles(root: &Path, out: &mut Vec<PrimitiveFacets>) {
|
||||
if !root.is_dir() {
|
||||
return;
|
||||
}
|
||||
for entry in WalkDir::new(root).max_depth(2).follow_links(false).into_iter().flatten() {
|
||||
let p = entry.path();
|
||||
if p.is_file() && p.extension().and_then(|s| s.to_str()) == Some("toml") {
|
||||
if let Ok(Some(pf)) = parse_primitive(p) {
|
||||
out.push(pf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse `k=v` filter strings into pairs. Bad entries (no `=`) are dropped.
|
||||
pub fn parse_filters(raw: &[String]) -> Vec<(String, String)> {
|
||||
raw.iter()
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use clap::{Parser, Subcommand};
|
|||
use kei_sage::atom_cli::{
|
||||
cmd_atoms_discover, cmd_atoms_rank, cmd_atoms_related, cmd_atoms_search, cmd_author,
|
||||
cmd_facet_query, cmd_lineage, cmd_rules_discover, default_atoms_root,
|
||||
default_capabilities_root, default_manifests_root, default_rules_root,
|
||||
default_capabilities_root, default_manifests_root, default_roles_root, default_rules_root,
|
||||
};
|
||||
use kei_sage::bfs::bfs;
|
||||
use kei_sage::edges::add_edge;
|
||||
|
|
@ -67,7 +67,7 @@ enum Cmd {
|
|||
FacetQuery {
|
||||
filters: Vec<String>,
|
||||
#[arg(long)] capabilities_root: Option<PathBuf>,
|
||||
#[arg(long)] manifests_root: Option<PathBuf>,
|
||||
#[arg(long)] manifests_root: Option<PathBuf>, #[arg(long)] roles_root: Option<PathBuf>,
|
||||
},
|
||||
Lineage {
|
||||
id: String,
|
||||
|
|
@ -117,9 +117,9 @@ fn dispatch(store: &Store, cmd: Cmd) -> anyhow::Result<()> {
|
|||
cmd_atoms_search(store, &root.unwrap_or_else(default_atoms_root), &query, limit),
|
||||
Cmd::AtomsRulesDiscover { rules_root } =>
|
||||
cmd_rules_discover(&rules_root.unwrap_or_else(default_rules_root)),
|
||||
Cmd::FacetQuery { filters, capabilities_root, manifests_root } => {
|
||||
Cmd::FacetQuery { filters, capabilities_root, manifests_root, roles_root } => {
|
||||
let (c, m) = prim_roots(capabilities_root, manifests_root);
|
||||
cmd_facet_query(&c, &m, &filters)
|
||||
cmd_facet_query(&c, &m, &roles_root.unwrap_or_else(default_roles_root), &filters)
|
||||
}
|
||||
Cmd::Lineage { id, depth, capabilities_root, manifests_root } => {
|
||||
let (c, m) = prim_roots(capabilities_root, manifests_root);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
//! Smoke tests for facet-query over capability.toml primitives.
|
||||
|
||||
use kei_sage::facet_query::{discover_primitives, matches_all, parse_filters};
|
||||
use kei_sage::facet_query::{
|
||||
discover_primitives, discover_primitives_with_roles, matches_all, parse_filters,
|
||||
};
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
|
|
@ -78,3 +80,63 @@ fn single_filter_matches_subset() {
|
|||
assert_eq!(hits.len(), 1);
|
||||
assert_eq!(hits[0].full_id, "scope::files-whitelist");
|
||||
}
|
||||
|
||||
const ROLE_READ_ONLY: &str = r#"
|
||||
[role]
|
||||
name = "read-only"
|
||||
|
||||
[taxonomy]
|
||||
kingdom = "role"
|
||||
mechanism = "compose"
|
||||
domain = "agent"
|
||||
"#;
|
||||
|
||||
const ROLE_GIT_OPS: &str = r#"
|
||||
[role]
|
||||
name = "git-ops"
|
||||
|
||||
[taxonomy]
|
||||
kingdom = "role"
|
||||
mechanism = "compose"
|
||||
domain = "agent"
|
||||
"#;
|
||||
|
||||
fn write_role(root: &std::path::Path, name: &str, body: &str) {
|
||||
fs::create_dir_all(root).unwrap();
|
||||
fs::write(root.join(format!("{name}.toml")), body).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn role_taxonomy_discovered_via_roles_root() {
|
||||
let cap = tempdir().unwrap();
|
||||
let man = tempdir().unwrap();
|
||||
let roles = tempdir().unwrap();
|
||||
write_role(roles.path(), "read-only", ROLE_READ_ONLY);
|
||||
write_role(roles.path(), "git-ops", ROLE_GIT_OPS);
|
||||
|
||||
let all = discover_primitives_with_roles(cap.path(), man.path(), Some(roles.path()));
|
||||
let filters = parse_filters(&["kingdom=role".into()]);
|
||||
let hits: Vec<_> = all.iter().filter(|p| matches_all(p, &filters)).collect();
|
||||
assert_eq!(hits.len(), 2);
|
||||
let ids: Vec<&str> = hits.iter().map(|p| p.full_id.as_str()).collect();
|
||||
assert!(ids.contains(&"role::read-only"));
|
||||
assert!(ids.contains(&"role::git-ops"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backward_compat_capability_still_matches_without_roles() {
|
||||
let cap = tempdir().unwrap();
|
||||
let man = tempdir().unwrap();
|
||||
let roles = tempdir().unwrap();
|
||||
write_cap(cap.path(), "policy", "no-git-ops", CAP_GATE);
|
||||
write_cap(cap.path(), "scope", "files-whitelist", CAP_SCOPE);
|
||||
write_role(roles.path(), "read-only", ROLE_READ_ONLY);
|
||||
|
||||
let all = discover_primitives_with_roles(cap.path(), man.path(), Some(roles.path()));
|
||||
let filters = parse_filters(&["kingdom=capability".into()]);
|
||||
let hits: Vec<_> = all.iter().filter(|p| matches_all(p, &filters)).collect();
|
||||
assert_eq!(hits.len(), 2, "role entry must NOT match kingdom=capability");
|
||||
let ids: Vec<&str> = hits.iter().map(|p| p.full_id.as_str()).collect();
|
||||
assert!(ids.contains(&"policy::no-git-ops"));
|
||||
assert!(ids.contains(&"scope::files-whitelist"));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue