use std::collections::BTreeSet;
use std::path::Path;
use crate::frontmatter::WikiLifecycle;
use crate::support::scope::scope_includes_page;
use crate::{CommandOutcome, ScopeSelection, WikiError, audit, lint};
pub(crate) fn execute(selection: ScopeSelection) -> Result<CommandOutcome, WikiError> {
super::run_analysis_command(
"audit",
selection,
"serialize audit report",
|root, output_scope| {
let report =
audit::run_with_options(root, output_scope, audit::AuditOptions::from_env())?;
promote_verified_lifecycle(root, &report)?;
Ok(report)
},
audit::render_text,
)
}
fn promote_verified_lifecycle(
vault_root: &Path,
report: &audit::AuditReport,
) -> Result<(), WikiError> {
let unsupported: BTreeSet<&Path> = report
.unsupported_claims
.iter()
.map(|claim| claim.path.as_path())
.collect();
for page in lint::collect_pages(vault_root)? {
if !scope_includes_page(&report.scope, &page.relative_path) {
continue;
}
if page.parsed.frontmatter.lifecycle != Some(WikiLifecycle::Reviewed) {
continue;
}
if unsupported.contains(page.relative_path.as_path()) {
continue;
}
crate::lifecycle::apply_lifecycle_transition(
vault_root,
&report.scope,
&page.relative_path,
WikiLifecycle::Verified,
"audit: citation pass with no unsupported claims",
)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::ScopeIdentity;
use crate::frontmatter::parse_frontmatter;
fn write_page(root: &Path, relative: &str, markdown: &str) {
let path = root.join(relative);
std::fs::create_dir_all(path.parent().expect("page parent")).expect("create parent");
std::fs::write(path, markdown).expect("write page");
}
fn report_with_unsupported(root: &Path, unsupported_paths: &[&str]) -> audit::AuditReport {
let source_context = Arc::new(Vec::new());
audit::AuditReport {
command: "audit",
scope: ScopeIdentity::project("proj"),
root: root.to_path_buf(),
claims: Vec::new(),
unsupported_claims: unsupported_paths
.iter()
.map(|path| audit::UnsupportedClaim {
path: (*path).into(),
line: 1,
heading: None,
claim: "claim".to_string(),
reason: "no citation".to_string(),
source_context: Arc::clone(&source_context),
})
.collect(),
source_context,
}
}
#[test]
fn citation_pass_promotes_reviewed_pages_to_verified() {
let temp = tempfile::tempdir().expect("tempdir");
write_page(
temp.path(),
"knowledge/concepts/supported.md",
"---\ntitle: Supported\nlifecycle: reviewed\n---\n\nBody.\n",
);
write_page(
temp.path(),
"knowledge/concepts/unsupported.md",
"---\ntitle: Unsupported\nlifecycle: reviewed\n---\n\nBody.\n",
);
write_page(
temp.path(),
"knowledge/concepts/draft.md",
"---\ntitle: Draft\nlifecycle: draft\n---\n\nBody.\n",
);
let report = report_with_unsupported(temp.path(), &["knowledge/concepts/unsupported.md"]);
promote_verified_lifecycle(temp.path(), &report).expect("promotion pass");
let lifecycle_of = |relative: &str| {
let markdown = std::fs::read_to_string(temp.path().join(relative)).expect("read page");
parse_frontmatter(&markdown)
.expect("parse")
.metadata
.lifecycle
};
assert_eq!(
lifecycle_of("knowledge/concepts/supported.md"),
Some(WikiLifecycle::Verified)
);
assert_eq!(
lifecycle_of("knowledge/concepts/unsupported.md"),
Some(WikiLifecycle::Reviewed)
);
assert_eq!(
lifecycle_of("knowledge/concepts/draft.md"),
Some(WikiLifecycle::Draft)
);
let log = std::fs::read_to_string(temp.path().join("log.md")).expect("read log");
assert_eq!(log.matches("lifecycle_transition:").count(), 1, "{log}");
assert!(log.contains("reviewed -> verified"), "{log}");
}
}