use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use super::candidate::Derivation;
use super::rule_file;
use crate::core::archive::authority::without_capture;
use crate::core::archive::bindings::{self, Item};
use crate::core::context_admission::carrier::Withheld;
use crate::core::context_admission::stored::{self, Lineage, SourceRef, StoredBinding};
const ORIGIN: &str = "skillify:rule";
const MAX_SIDECAR_BYTES: u64 = 256 * 1024;
const MAX_WALK: usize = 256;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Notice {
Edited,
Unbound,
NotMoved,
}
impl Notice {
pub(super) fn code(self) -> &'static str {
match self {
Self::Edited => "rule.edited_source_withdrawn",
Self::Unbound => "rule.no_source_on_record",
Self::NotMoved => "rule.not_moved",
}
}
pub(super) fn sentence(self) -> &'static str {
match self {
Self::Edited => "source no longer admitted, file edited by hand — remove it yourself",
Self::Unbound => "no source on record, lean-ctx cannot check it — review it yourself",
Self::NotMoved => {
"source no longer admitted, file could not be moved — remove it yourself"
}
}
}
}
#[derive(Serialize, Deserialize)]
struct Authority {
rule: PathBuf,
binding: StoredBinding,
}
fn store_dir(name: &str) -> Option<PathBuf> {
Some(
crate::core::data_dir::lean_ctx_data_dir()
.ok()?
.join("skillify")
.join(name),
)
}
fn key(rule: &Path) -> String {
let identity = crate::core::pathutil::canonicalize_secure_or_self(rule);
blake3::hash(identity.to_string_lossy().as_bytes())
.to_hex()
.to_string()
}
fn sidecar(rule: &Path) -> Option<PathBuf> {
Some(store_dir("authority")?.join(format!("{}.authority", key(rule))))
}
pub(super) fn quarantine_paths(rule: &Path) -> Option<(PathBuf, PathBuf)> {
let directory = store_dir("quarantine")?;
let key = key(rule);
Some((
directory.join(format!("{key}.mdc")),
directory.join(format!("{key}.origin")),
))
}
fn load(path: &Path) -> Option<Authority> {
let metadata = std::fs::symlink_metadata(path).ok()?;
if !metadata.is_file() || metadata.len() > MAX_SIDECAR_BYTES {
return None;
}
serde_json::from_str(&std::fs::read_to_string(path).ok()?).ok()
}
fn binding_of(rule: &Path, text: &str) -> Option<StoredBinding> {
load(&sidecar(rule)?)
.map(|authority| authority.binding)
.filter(|binding| binding.binds(text))
}
fn sources(
derivation: &Derivation,
reader: &str,
enforced: bool,
) -> Result<Option<(String, Vec<SourceRef>)>, Withheld> {
if derivation.unbound || derivation.facts.is_empty() {
return Ok(None);
}
let mut project: Option<String> = None;
let mut sources = Vec::new();
for (category, key, value) in &derivation.facts {
let item = Item::KnowledgeFact {
project: &derivation.store,
category,
key,
};
match bindings::held(&item, value, Some(reader)) {
Ok(Some((root, files))) => {
if project
.as_deref()
.is_some_and(|joined| joined != root.as_str())
{
return if enforced {
Err(Withheld::no_source_authority())
} else {
Ok(None)
};
}
project = Some(root);
sources.extend(files);
}
Err(withheld) if enforced => return Err(withheld),
Ok(None) | Err(_) => return Ok(None),
}
}
Ok(project.map(|project| (project, sources)))
}
pub(super) fn prepare(
text: &str,
derivation: &Derivation,
reader: &str,
) -> Result<Option<StoredBinding>, Withheld> {
let enforced = stored::source_authority_required();
let Some((project, sources)) = sources(derivation, reader, enforced)? else {
return if enforced {
Err(Withheld::no_source_authority())
} else {
Ok(None)
};
};
let bound = Lineage::of(sources)
.and_then(|lineage| stored::admit_and_bind(text, ORIGIN, Some(&project), lineage))
.and_then(|(admitted, binding)| {
if admitted == text {
Ok(binding)
} else {
Err(Withheld::new(&["policy.content_redacted"]))
}
});
match bound {
Ok(binding) => Ok(Some(binding)),
Err(withheld) if enforced => Err(withheld),
Err(_) => Ok(None),
}
}
pub(super) fn record(rule: &Path, binding: &StoredBinding) -> Result<(), String> {
let path = sidecar(rule).ok_or("cannot resolve the skillify authority directory")?;
let authority = Authority {
rule: crate::core::pathutil::canonicalize_secure_or_self(rule),
binding: binding.clone(),
};
let encoded = serde_json::to_string(&authority).map_err(|error| error.to_string())?;
crate::config_io::write_atomic(&path, &encoded)
}
pub(super) fn served(rule: &Path, text: &str, reader: &str) -> Result<(), Withheld> {
let binding = binding_of(rule, text);
let project = binding.as_ref().map(|_| reader);
stored::serve_stored(text, binding.as_ref(), project)?;
stored::publish_served(binding.as_ref());
Ok(())
}
pub(super) fn carry_over(from: &Path, to: &Path, text: &str) -> Result<(), String> {
match binding_of(from, text) {
Some(binding) => record(to, &binding),
None => Ok(()),
}
}
pub(super) fn release(rule: &Path) {
if let Some((copy, origin)) = quarantine_paths(rule) {
let _ = std::fs::remove_file(copy);
let _ = std::fs::remove_file(origin);
}
}
pub(super) fn review(output_root: &Path, reader: &str) -> Vec<(PathBuf, Notice)> {
forget_missing();
let mut reported = Vec::new();
if !stored::source_authority_required() {
return reported;
}
let Ok(entries) = std::fs::read_dir(rule_file::rules_dir(output_root)) else {
return reported;
};
for entry in entries.flatten().take(MAX_WALK) {
let path = entry.path();
let generated = entry.file_type().is_ok_and(|kind| kind.is_file())
&& path.extension().and_then(|e| e.to_str()) == Some("mdc")
&& crate::core::pathjail::jail_path(&path, output_root).is_ok();
let ours = generated
&& path
.file_stem()
.and_then(|stem| stem.to_str())
.is_some_and(|stem| stem.starts_with(rule_file::SLUG_PREFIX));
if !ours {
continue;
}
let Ok(text) = std::fs::read_to_string(&path) else {
continue;
};
let notice = match sidecar(&path).and_then(|sidecar| load(&sidecar)) {
None => Some(Notice::Unbound),
Some(authority) if authority.binding.binds(&text) => {
(withdrawn(&text, &authority.binding, reader) && quarantine(&path, &text).is_err())
.then_some(Notice::NotMoved)
}
Some(authority) => (!sources_hold(&authority.binding)).then_some(Notice::Edited),
};
if let Some(notice) = notice {
reported.push((path, notice));
}
}
reported.sort_by(|a, b| a.0.cmp(&b.0));
reported
}
fn withdrawn(text: &str, binding: &StoredBinding, reader: &str) -> bool {
without_capture(|| stored::serve_stored(text, Some(binding), Some(reader))).is_err_and(
|withheld| {
!withheld
.codes()
.iter()
.any(|code| code.starts_with("store.project_"))
},
)
}
fn sources_hold(binding: &StoredBinding) -> bool {
binding.file_sources().is_none_or(|(project, files)| {
without_capture(|| {
files.into_iter().all(|(path, tool, digest)| {
SourceRef::read_as_admitted(path, project, tool, digest).is_some()
})
})
})
}
fn quarantine(rule: &Path, text: &str) -> Result<(), String> {
let (copy, origin) = quarantine_paths(rule).ok_or("no skillify quarantine directory")?;
let authority = sidecar(rule);
let original = serde_json::to_string(&rule.to_string_lossy()).map_err(|e| e.to_string())?;
crate::config_io::write_atomic(©, text)?;
crate::config_io::write_atomic(&origin, &original)?;
let unchanged = std::fs::symlink_metadata(rule).is_ok_and(|metadata| metadata.is_file())
&& std::fs::read_to_string(rule).is_ok_and(|current| current == text);
if !unchanged {
return Err("rule file changed during the move".to_owned());
}
std::fs::remove_file(rule).map_err(|error| error.to_string())?;
if let Some(authority) = authority {
let _ = std::fs::remove_file(authority);
}
Ok(())
}
fn forget_missing() {
let Some(entries) = store_dir("authority").and_then(|dir| std::fs::read_dir(dir).ok()) else {
return;
};
for entry in entries.flatten().take(MAX_WALK) {
let path = entry.path();
let gone = load(&path).is_some_and(|authority| {
std::fs::symlink_metadata(&authority.rule)
.is_err_and(|error| error.kind() == std::io::ErrorKind::NotFound)
});
if gone {
let _ = std::fs::remove_file(&path);
}
}
}