use std::path::{Path, PathBuf};
use harn_lexer::Lexer;
use harn_parser::{peel_attributes, Attribute, AttributeArg, Node, Parser};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use super::discovery::{
parse_invariants_source, ArchivistMetadata, DiagnosticSeverity, DiscoveryDiagnostic,
};
use super::result::{Approver, InvariantBlockError, Verdict};
use crate::flow::slice::PredicateHash;
pub const META_INVARIANTS_FILE: &str = "meta-invariants.harn";
pub const DEFAULT_MAINTAINER_ROLE: &str = "flow-platform";
pub mod codes {
pub const PARSE_ERROR: &str = "bootstrap_parse_error";
pub const KIND_COLLISION: &str = "bootstrap_kind_collision";
pub const MISSING_ARCHIVIST: &str = "bootstrap_missing_archivist";
pub const ARCHIVIST_PROVENANCE_INCOMPLETE: &str = "bootstrap_archivist_provenance_incomplete";
pub const MISSING_SEMANTIC_FALLBACK: &str = "bootstrap_missing_semantic_fallback";
pub const UNRESOLVED_SEMANTIC_FALLBACK: &str = "bootstrap_unresolved_semantic_fallback";
pub const ARCHIVIST_AUTHORED_BOOTSTRAP: &str = "bootstrap_archivist_cannot_author_bootstrap";
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum EditAuthor {
Archivist,
Human { id: String },
System { id: String },
}
impl EditAuthor {
pub fn human(id: impl Into<String>) -> Self {
Self::Human { id: id.into() }
}
pub fn system(id: impl Into<String>) -> Self {
Self::System { id: id.into() }
}
fn label(&self) -> String {
match self {
EditAuthor::Archivist => "archivist".to_string(),
EditAuthor::Human { id } => format!("human:{id}"),
EditAuthor::System { id } => format!("system:{id}"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BootstrapViolation {
pub code: String,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub predicate: Option<String>,
}
impl BootstrapViolation {
fn new(code: &'static str, message: impl Into<String>) -> Self {
Self {
code: code.to_string(),
message: message.into(),
predicate: None,
}
}
fn with_predicate(mut self, predicate: impl Into<String>) -> Self {
self.predicate = Some(predicate.into());
self
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BootstrapPolicy {
pub hash: PredicateHash,
pub maintainers: Vec<Approver>,
}
impl BootstrapPolicy {
pub fn parse(source: &str) -> Self {
Self::parse_with_diagnostics(source).0
}
pub fn parse_with_diagnostics(source: &str) -> (Self, Vec<DiscoveryDiagnostic>) {
let hash = bootstrap_hash(source);
let parsed = parse_invariants_source(source);
let mut diagnostics = parsed.diagnostics;
let mut maintainers = Vec::new();
match collect_top_level_attributes(source) {
Ok(attrs) => {
for attr in attrs {
if attr.name == "bootstrap_maintainers" {
maintainers.extend(parse_maintainers(&attr.args));
}
}
}
Err(diagnostic) => diagnostics.push(diagnostic),
}
if maintainers.is_empty() {
maintainers.push(Approver::role(DEFAULT_MAINTAINER_ROLE));
}
(Self { hash, maintainers }, diagnostics)
}
}
#[derive(Clone, Debug)]
pub struct DiscoveredBootstrapPolicy {
pub path: PathBuf,
pub source: String,
pub policy: BootstrapPolicy,
pub diagnostics: Vec<DiscoveryDiagnostic>,
}
pub fn discover_bootstrap_policy(root: &Path) -> Option<DiscoveredBootstrapPolicy> {
let path = root.join(META_INVARIANTS_FILE);
if !path.is_file() {
return None;
}
let source = std::fs::read_to_string(&path).ok()?;
let (policy, diagnostics) = BootstrapPolicy::parse_with_diagnostics(&source);
Some(DiscoveredBootstrapPolicy {
path,
source,
policy,
diagnostics,
})
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BootstrapValidation {
pub verdict: Verdict,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub previous_policy_hash: Option<PredicateHash>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proposed_policy_hash: Option<PredicateHash>,
pub author: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub violations: Vec<BootstrapViolation>,
}
impl BootstrapValidation {
pub fn is_blocked(&self) -> bool {
matches!(self.verdict, Verdict::Block { .. })
}
pub fn requires_approval(&self) -> bool {
matches!(self.verdict, Verdict::RequireApproval { .. })
}
}
pub fn validate_predicate_edit(
proposed_source: &str,
author: &EditAuthor,
previous_policy: Option<&BootstrapPolicy>,
) -> BootstrapValidation {
let violations = collect_predicate_edit_violations(proposed_source);
let verdict = if violations.is_empty() {
Verdict::Allow
} else {
Verdict::Block {
error: build_block_error(codes::PARSE_ERROR, &violations),
}
};
BootstrapValidation {
verdict,
previous_policy_hash: previous_policy.map(|policy| policy.hash.clone()),
proposed_policy_hash: None,
author: author.label(),
violations,
}
}
pub fn validate_bootstrap_edit(
proposed_source: &str,
author: &EditAuthor,
previous_policy: Option<&BootstrapPolicy>,
) -> BootstrapValidation {
let proposed_hash = bootstrap_hash(proposed_source);
let previous_hash = previous_policy.map(|policy| policy.hash.clone());
let mut violations = Vec::new();
if matches!(author, EditAuthor::Archivist) {
violations.push(BootstrapViolation::new(
codes::ARCHIVIST_AUTHORED_BOOTSTRAP,
"Archivist persona is propose-only and may not author or promote \
meta-invariants.harn edits — escalate to a human maintainer",
));
let error = build_block_error(codes::ARCHIVIST_AUTHORED_BOOTSTRAP, &violations);
return BootstrapValidation {
verdict: Verdict::Block { error },
previous_policy_hash: previous_hash,
proposed_policy_hash: Some(proposed_hash),
author: author.label(),
violations,
};
}
let (parsed_policy, parse_diagnostics) =
BootstrapPolicy::parse_with_diagnostics(proposed_source);
for diagnostic in parse_diagnostics
.iter()
.filter(|d| d.severity == DiagnosticSeverity::Error)
{
violations.push(BootstrapViolation::new(
codes::PARSE_ERROR,
diagnostic.message.clone(),
));
}
if !violations.is_empty() {
let error = build_block_error(codes::PARSE_ERROR, &violations);
return BootstrapValidation {
verdict: Verdict::Block { error },
previous_policy_hash: previous_hash,
proposed_policy_hash: Some(proposed_hash),
author: author.label(),
violations,
};
}
let approver = previous_policy
.and_then(|policy| policy.maintainers.first().cloned())
.or_else(|| parsed_policy.maintainers.first().cloned())
.unwrap_or_else(|| Approver::role(DEFAULT_MAINTAINER_ROLE));
BootstrapValidation {
verdict: Verdict::RequireApproval { approver },
previous_policy_hash: previous_hash,
proposed_policy_hash: Some(proposed_hash),
author: author.label(),
violations,
}
}
fn collect_predicate_edit_violations(proposed_source: &str) -> Vec<BootstrapViolation> {
let parsed = parse_invariants_source(proposed_source);
let mut violations = Vec::new();
for diagnostic in &parsed.diagnostics {
if diagnostic.severity != DiagnosticSeverity::Error {
continue;
}
let code = if diagnostic.message.contains("pick exactly one") {
codes::KIND_COLLISION
} else if diagnostic
.message
.contains("must declare a deterministic fallback")
{
codes::MISSING_SEMANTIC_FALLBACK
} else if diagnostic
.message
.contains("same invariants.harn file or an ancestor file")
{
codes::UNRESOLVED_SEMANTIC_FALLBACK
} else {
codes::PARSE_ERROR
};
violations.push(BootstrapViolation::new(code, diagnostic.message.clone()));
}
for predicate in &parsed.predicates {
match predicate.archivist.as_ref() {
None => {
violations.push(
BootstrapViolation::new(
codes::MISSING_ARCHIVIST,
format!(
"predicate `{}` must declare `@archivist(...)` provenance \
before promotion",
predicate.name
),
)
.with_predicate(&predicate.name),
);
}
Some(archivist) => {
for missing in archivist_missing_fields(archivist) {
violations.push(
BootstrapViolation::new(
codes::ARCHIVIST_PROVENANCE_INCOMPLETE,
format!(
"predicate `{}` is missing required @archivist field `{missing}`",
predicate.name
),
)
.with_predicate(&predicate.name),
);
}
}
}
}
deduplicate_violations(violations)
}
fn deduplicate_violations(violations: Vec<BootstrapViolation>) -> Vec<BootstrapViolation> {
let mut seen = std::collections::BTreeSet::<(String, String, Option<String>)>::new();
let mut out = Vec::with_capacity(violations.len());
for violation in violations {
let key = (
violation.code.to_string(),
violation.message.clone(),
violation.predicate.clone(),
);
if seen.insert(key) {
out.push(violation);
}
}
out
}
fn archivist_missing_fields(archivist: &ArchivistMetadata) -> Vec<&'static str> {
let mut missing = Vec::new();
if archivist.evidence.is_empty() {
missing.push("evidence");
}
if archivist.confidence.is_none() {
missing.push("confidence");
}
if archivist.source_date.is_none() {
missing.push("source_date");
}
if archivist.coverage_examples.is_empty() {
missing.push("coverage_examples");
}
missing
}
fn build_block_error(code: &'static str, violations: &[BootstrapViolation]) -> InvariantBlockError {
let summary = violations
.iter()
.map(|v| {
if let Some(predicate) = &v.predicate {
format!("{} (in `{predicate}`): {}", v.code, v.message)
} else {
format!("{}: {}", v.code, v.message)
}
})
.collect::<Vec<_>>()
.join("; ");
let message = if summary.is_empty() {
"bootstrap policy rejected the proposed edit".to_string()
} else {
format!("bootstrap policy rejected the proposed edit: {summary}")
};
InvariantBlockError::new(code, message)
}
fn bootstrap_hash(source: &str) -> PredicateHash {
PredicateHash::new(format!(
"sha256:{}",
hex::encode(Sha256::digest(source.as_bytes()))
))
}
fn parse_maintainers(args: &[AttributeArg]) -> Vec<Approver> {
args.iter()
.flat_map(|arg| match &arg.value.node {
Node::ListLiteral(items) => items
.iter()
.filter_map(|item| match &item.node {
Node::StringLiteral(s) | Node::RawStringLiteral(s) => {
Some(parse_maintainer_str(s))
}
_ => None,
})
.collect::<Vec<_>>(),
Node::StringLiteral(s) | Node::RawStringLiteral(s) => {
vec![parse_maintainer_str(s)]
}
_ => Vec::new(),
})
.collect()
}
fn parse_maintainer_str(value: &str) -> Approver {
if let Some(role) = value.strip_prefix("role:") {
Approver::role(role.trim())
} else if let Some(principal) = value.strip_prefix("user:") {
Approver::principal(format!("user:{}", principal.trim()))
} else {
Approver::principal(value.trim())
}
}
fn collect_top_level_attributes(source: &str) -> Result<Vec<Attribute>, DiscoveryDiagnostic> {
let tokens = Lexer::new(source)
.tokenize()
.map_err(|error| DiscoveryDiagnostic {
severity: DiagnosticSeverity::Error,
message: format!("lex error: {error:?}"),
span: None,
})?;
let program = Parser::new(tokens)
.parse()
.map_err(|error| DiscoveryDiagnostic {
severity: DiagnosticSeverity::Error,
message: format!("parse error: {error:?}"),
span: None,
})?;
let mut out = Vec::new();
for node in &program {
let (attrs, _inner) = peel_attributes(node);
out.extend(attrs.iter().cloned());
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
const VALID_PREDICATE: &str = r#"
@invariant
@deterministic
@archivist(
evidence: ["https://example.com/spec"],
confidence: 0.95,
source_date: "2026-04-26",
coverage_examples: ["crates/api/src/auth.rs"]
)
fn no_raw_tokens(slice) {
return flow_invariant_allow()
}
"#;
const VALID_BOOTSTRAP: &str = r#"
@bootstrap_maintainers(approvers: ["role:flow-platform", "user:alice"])
fn _meta_invariants_marker() {
return nil
}
"#;
#[test]
fn parses_bootstrap_policy_hash_and_maintainers() {
let policy = BootstrapPolicy::parse(VALID_BOOTSTRAP);
assert!(policy.hash.as_str().starts_with("sha256:"));
assert_eq!(policy.maintainers.len(), 2);
assert!(matches!(
policy.maintainers[0],
Approver::Role { ref name } if name == "flow-platform"
));
assert!(matches!(
policy.maintainers[1],
Approver::Principal { ref id } if id == "user:alice"
));
}
#[test]
fn bootstrap_hash_is_stable_across_identical_sources() {
let a = BootstrapPolicy::parse(VALID_BOOTSTRAP);
let b = BootstrapPolicy::parse(VALID_BOOTSTRAP);
assert_eq!(a.hash, b.hash);
}
#[test]
fn bootstrap_hash_changes_when_source_changes() {
let a = BootstrapPolicy::parse(VALID_BOOTSTRAP);
let b = BootstrapPolicy::parse(&VALID_BOOTSTRAP.replace("alice", "bob"));
assert_ne!(a.hash, b.hash);
}
#[test]
fn bootstrap_default_maintainer_when_attribute_missing() {
let policy = BootstrapPolicy::parse("// no maintainer attribute here\n");
assert_eq!(policy.maintainers.len(), 1);
assert!(matches!(
policy.maintainers[0],
Approver::Role { ref name } if name == DEFAULT_MAINTAINER_ROLE
));
}
#[test]
fn validate_initial_seed_predicate_edit_passes_without_prior_policy() {
let result = validate_predicate_edit(VALID_PREDICATE, &EditAuthor::Archivist, None);
assert!(matches!(result.verdict, Verdict::Allow), "{result:?}");
assert_eq!(result.previous_policy_hash, None);
assert!(result.violations.is_empty());
assert_eq!(result.author, "archivist");
}
#[test]
fn validate_normal_predicate_edit_pins_previous_policy_hash() {
let policy = BootstrapPolicy::parse(VALID_BOOTSTRAP);
let result = validate_predicate_edit(
VALID_PREDICATE,
&EditAuthor::human("user:alice"),
Some(&policy),
);
assert!(matches!(result.verdict, Verdict::Allow));
assert_eq!(result.previous_policy_hash, Some(policy.hash.clone()));
}
#[test]
fn validate_predicate_edit_blocks_when_archivist_provenance_missing() {
let source = r#"
@invariant
@deterministic
fn no_provenance(slice) { return true }
"#;
let result = validate_predicate_edit(source, &EditAuthor::Archivist, None);
assert!(result.is_blocked());
let codes: Vec<&str> = result.violations.iter().map(|v| v.code.as_str()).collect();
assert!(codes.contains(&codes::MISSING_ARCHIVIST), "{codes:?}");
}
#[test]
fn validate_predicate_edit_blocks_when_archivist_provenance_partial() {
let source = r#"
@invariant
@deterministic
@archivist(evidence: ["https://x"])
fn partial_provenance(slice) { return true }
"#;
let result = validate_predicate_edit(source, &EditAuthor::Archivist, None);
assert!(result.is_blocked());
let missing_fields: Vec<String> = result
.violations
.iter()
.filter(|v| v.code == codes::ARCHIVIST_PROVENANCE_INCOMPLETE)
.map(|v| v.message.clone())
.collect();
assert!(
missing_fields.iter().any(|m| m.contains("confidence")),
"{missing_fields:?}"
);
assert!(
missing_fields.iter().any(|m| m.contains("source_date")),
"{missing_fields:?}"
);
assert!(
missing_fields
.iter()
.any(|m| m.contains("coverage_examples")),
"{missing_fields:?}"
);
}
#[test]
fn validate_predicate_edit_blocks_when_kinds_collide() {
let source = r#"
@invariant
@deterministic
@semantic
@archivist(evidence: ["x"], confidence: 0.5, source_date: "2026-04-26", coverage_examples: ["a"])
fn dual_mode(slice) { return true }
"#;
let result = validate_predicate_edit(source, &EditAuthor::Archivist, None);
assert!(result.is_blocked());
let codes: Vec<&str> = result.violations.iter().map(|v| v.code.as_str()).collect();
assert!(codes.contains(&codes::KIND_COLLISION), "{codes:?}");
}
#[test]
fn validate_predicate_edit_blocks_when_semantic_fallback_missing() {
let source = r#"
@invariant
@semantic
@archivist(evidence: ["x"], confidence: 0.5, source_date: "2026-04-26", coverage_examples: ["a"])
fn semantic_no_fallback(slice) { return true }
"#;
let result = validate_predicate_edit(source, &EditAuthor::Archivist, None);
assert!(result.is_blocked());
let codes: Vec<&str> = result.violations.iter().map(|v| v.code.as_str()).collect();
assert!(
codes.contains(&codes::MISSING_SEMANTIC_FALLBACK),
"{codes:?}"
);
}
#[test]
fn validate_bootstrap_edit_rejects_archivist_authorship() {
let previous = BootstrapPolicy::parse(VALID_BOOTSTRAP);
let proposed = VALID_BOOTSTRAP.replace("alice", "mallory");
let result = validate_bootstrap_edit(&proposed, &EditAuthor::Archivist, Some(&previous));
assert!(result.is_blocked());
let codes: Vec<&str> = result.violations.iter().map(|v| v.code.as_str()).collect();
assert_eq!(codes, vec![codes::ARCHIVIST_AUTHORED_BOOTSTRAP]);
assert_eq!(result.previous_policy_hash, Some(previous.hash));
assert!(result.proposed_policy_hash.is_some());
}
#[test]
fn validate_bootstrap_edit_routes_human_to_require_approval() {
let previous = BootstrapPolicy::parse(VALID_BOOTSTRAP);
let proposed = VALID_BOOTSTRAP.replace("alice", "carol");
let result =
validate_bootstrap_edit(&proposed, &EditAuthor::human("user:carol"), Some(&previous));
assert!(result.requires_approval(), "{result:?}");
let approver = match &result.verdict {
Verdict::RequireApproval { approver } => approver.clone(),
other => panic!("expected RequireApproval, got {other:?}"),
};
assert!(matches!(approver, Approver::Role { ref name } if name == "flow-platform"));
assert_eq!(result.previous_policy_hash, Some(previous.hash));
}
#[test]
fn validate_bootstrap_edit_initial_seed_uses_default_role() {
let result =
validate_bootstrap_edit("// initial seed\n", &EditAuthor::human("user:alice"), None);
assert!(result.requires_approval());
let approver = match &result.verdict {
Verdict::RequireApproval { approver } => approver.clone(),
other => panic!("expected RequireApproval, got {other:?}"),
};
assert!(matches!(
approver,
Approver::Role { ref name } if name == DEFAULT_MAINTAINER_ROLE
));
assert_eq!(result.previous_policy_hash, None);
}
#[test]
fn validate_bootstrap_edit_blocks_unparseable_source() {
let proposed = r#"
@invariant
@deterministic
@semantic
@archivist(evidence: ["x"])
fn bad(slice) { return true }
"#;
let previous = BootstrapPolicy::parse(VALID_BOOTSTRAP);
let result =
validate_bootstrap_edit(proposed, &EditAuthor::human("user:alice"), Some(&previous));
assert!(result.is_blocked(), "{result:?}");
}
#[test]
fn discover_returns_none_when_file_missing() {
let tmp = TempDir::new().unwrap();
assert!(discover_bootstrap_policy(tmp.path()).is_none());
}
#[test]
fn discover_loads_meta_invariants_from_root() {
let tmp = TempDir::new().unwrap();
fs::write(tmp.path().join(META_INVARIANTS_FILE), VALID_BOOTSTRAP).unwrap();
let discovered = discover_bootstrap_policy(tmp.path()).expect("policy present");
assert!(discovered.path.ends_with(META_INVARIANTS_FILE));
assert_eq!(discovered.policy.maintainers.len(), 2);
assert_eq!(discovered.source, VALID_BOOTSTRAP);
}
}