use crate::light_author_handlers::{
render_light_markdown, LightAuthorInput, LightClaim, SupersessionOutcome,
};
use crate::util::now_ms;
use m1nd_core::error::{M1ndError, M1ndResult};
use serde_json::{json, Value};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct PromoteInput {
pub agent_id: String,
pub brain: String,
pub claim: String,
pub reason: String,
}
fn evidence_class_gate(frontmatter: &ClaimFrontmatter) -> Result<(), String> {
let state_ok = frontmatter
.state
.as_deref()
.map(|s| s.trim().eq_ignore_ascii_case("verified"))
.unwrap_or(false);
let founder_ok = frontmatter
.source_agent
.as_deref()
.map(|a| a.trim().eq_ignore_ascii_case("human:maintainer"))
.unwrap_or(false);
if state_ok || founder_ok {
Ok(())
} else {
Err(format!(
"promotion refused (C8.3 evidence-class gate): only a claim with State: verified \
or Source-Agent: human:maintainer may promote — this claim is State: {} / \
Source-Agent: {}. Verify it in its home brain first, then promote.",
frontmatter.state.as_deref().unwrap_or("unknown"),
frontmatter.source_agent.as_deref().unwrap_or("unknown"),
))
}
}
fn hygiene_floor(text: &str) -> Result<(), String> {
for marker in ["<<<<<<<", "=======", ">>>>>>>"] {
if text.lines().any(|l| l.starts_with(marker)) {
return Err(format!(
"promotion refused (hygiene floor): the claim text carries a merge-conflict \
marker ('{marker}') — resolve it in the source brain before promoting."
));
}
}
const SECRET_MARKERS: &[&str] = &[
"-----BEGIN ", "aws_secret_access", "AKIA", "sk-", "ghp_", "xoxb-", "xoxp-", ];
let lower = text.to_ascii_lowercase();
for marker in SECRET_MARKERS {
if lower.contains(&marker.to_ascii_lowercase()) {
return Err(format!(
"promotion refused (hygiene floor): the claim text matches a secret shape \
('{}') — the medulla is the most-read store and must never carry a credential. \
Redact it in the source brain before promoting.",
marker.trim()
));
}
}
Ok(())
}
#[derive(Debug, Default, Clone)]
pub struct ClaimFrontmatter {
pub node: Option<String>,
pub state: Option<String>,
pub source_agent: Option<String>,
pub origin_brain: Option<String>,
}
pub struct ParsedClaim {
pub frontmatter: ClaimFrontmatter,
pub title: Option<String>,
pub claims: Vec<LightClaim>,
}
pub fn parse_light_claim(text: &str) -> ParsedClaim {
let mut fm = ClaimFrontmatter::default();
let mut title: Option<String> = None;
let mut claims: Vec<LightClaim> = Vec::new();
let mut in_frontmatter = false;
let mut frontmatter_done = false;
let mut pending_prose: Option<String> = None;
for raw in text.lines() {
let line = raw.trim_end();
let trimmed = line.trim();
if trimmed == "---" {
if !in_frontmatter && !frontmatter_done {
in_frontmatter = true;
} else if in_frontmatter {
in_frontmatter = false;
frontmatter_done = true;
}
continue;
}
if in_frontmatter {
if let Some(v) = trimmed.strip_prefix("Node:") {
fm.node = Some(v.trim().to_string());
} else if let Some(v) = trimmed.strip_prefix("State:") {
fm.state = Some(v.trim().to_string());
} else if let Some(v) = trimmed.strip_prefix("Source-Agent:") {
fm.source_agent = Some(v.trim().to_string());
} else if let Some(v) = trimmed.strip_prefix("Origin-Brain:") {
fm.origin_brain = Some(v.trim().to_string());
}
continue;
}
if let Some(v) = trimmed.strip_prefix("## ") {
if title.is_none() {
title = Some(v.trim().to_string());
}
pending_prose = None;
continue;
}
if trimmed.starts_with("# ") {
continue;
}
if let Some((kind, label)) = parse_entity_marker(trimmed) {
claims.push(LightClaim {
label,
text: pending_prose.take(),
kind: Some(kind.to_string()),
confidence: None,
ambiguity: None,
evidence: Vec::new(),
depends_on: Vec::new(),
});
continue;
}
if let Some(rest) = trimmed.strip_prefix("[𝔻 confidence:") {
if let Some(c) = claims.last_mut() {
c.confidence = Some(rest.trim_end_matches(']').trim().to_string());
}
continue;
}
if let Some(rest) = trimmed.strip_prefix("[𝔻 ambiguity:") {
if let Some(c) = claims.last_mut() {
c.ambiguity = Some(rest.trim_end_matches(']').trim().to_string());
}
continue;
}
if let Some(rest) = trimmed.strip_prefix("[𝔻 evidence:") {
if let Some(c) = claims.last_mut() {
c.evidence
.push(rest.trim_end_matches(']').trim().to_string());
}
continue;
}
if let Some(rest) = trimmed.strip_prefix("[⟁ depends_on:") {
if let Some(c) = claims.last_mut() {
c.depends_on
.push(rest.trim_end_matches(']').trim().to_string());
}
continue;
}
if !trimmed.is_empty() {
pending_prose = Some(trimmed.to_string());
}
}
ParsedClaim {
frontmatter: fm,
title,
claims,
}
}
fn parse_entity_marker(line: &str) -> Option<(&'static str, String)> {
for (glyph, kind) in [("⍂", "entity"), ("⍐", "state"), ("⍌", "event")] {
let prefix = format!("[{glyph} {kind}:");
if let Some(rest) = line.strip_prefix(&prefix) {
let label = rest.trim_end_matches(']').trim().to_string();
return Some((kind, label));
}
}
None
}
pub struct EvidenceReanchor {
pub claims: Vec<LightClaim>,
pub origin_qualified: bool,
pub evidence_unverifiable: bool,
}
pub fn reanchor_evidence(claims: &[LightClaim], origin_root: Option<&str>) -> EvidenceReanchor {
let has_evidence = claims.iter().any(|c| !c.evidence.is_empty());
if !has_evidence {
return EvidenceReanchor {
claims: claims.to_vec(),
origin_qualified: false,
evidence_unverifiable: false,
};
}
match origin_root {
Some(root) if !root.trim().is_empty() && root != "medulla" => {
let rewritten = claims
.iter()
.map(|c| {
let mut c2 = c.clone();
c2.evidence = c
.evidence
.iter()
.map(|e| {
if e.contains('#') {
e.clone()
} else {
format!("{root}#{e}")
}
})
.collect();
c2
})
.collect();
EvidenceReanchor {
claims: rewritten,
origin_qualified: true,
evidence_unverifiable: false,
}
}
_ => EvidenceReanchor {
claims: claims.to_vec(),
origin_qualified: false,
evidence_unverifiable: true,
},
}
}
pub struct PromoteOutcome {
pub medulla_path: PathBuf,
pub witness_path: PathBuf,
pub medulla_slug: String,
pub origin_brain: String,
pub origin_qualified: bool,
pub evidence_unverifiable: bool,
pub medulla_claim_count: usize,
pub soft_cap: usize,
}
pub const MEDULLA_SOFT_CAP: usize = 300;
pub fn promote_claim(
input: &PromoteInput,
source_store_dir: &Path,
medulla_store_dir: &Path,
medulla_runtime_root: &Path,
) -> M1ndResult<PromoteOutcome> {
let source_slug = crate::light_author_handlers::slugify(&input.claim);
let source_path = source_store_dir.join(format!("{source_slug}.light.md"));
if !source_path.exists() {
return Err(M1ndError::InvalidParams {
tool: "promote".into(),
detail: format!(
"no claim '{}' (slug '{source_slug}') in brain '{}' — nothing to promote (no guessing on an unknown slug).",
input.claim, input.brain
),
});
}
let source_text = std::fs::read_to_string(&source_path).map_err(M1ndError::Io)?;
let parsed = parse_light_claim(&source_text);
evidence_class_gate(&parsed.frontmatter).map_err(|detail| M1ndError::InvalidParams {
tool: "promote".into(),
detail,
})?;
hygiene_floor(&source_text).map_err(|detail| M1ndError::InvalidParams {
tool: "promote".into(),
detail,
})?;
let origin_brain = parsed
.frontmatter
.origin_brain
.clone()
.filter(|o| !o.trim().is_empty())
.unwrap_or_else(|| input.brain.clone());
let reanchor = reanchor_evidence(&parsed.claims, Some(origin_brain.as_str()));
let node_label = parsed
.frontmatter
.node
.clone()
.unwrap_or_else(|| input.claim.clone());
let medulla_slug = crate::light_author_handlers::slugify(&node_label);
let medulla_path = medulla_store_dir.join(format!("{medulla_slug}.light.md"));
let mut promoted_input = LightAuthorInput {
agent_id: input.agent_id.clone(),
node_label: node_label.clone(),
title: parsed.title.clone(),
state: parsed.frontmatter.state.clone(),
claims: reanchor.claims.clone(),
output_path: Some(medulla_path.to_string_lossy().to_string()),
namespace: None,
ingest_after: false,
mode: "merge".into(),
supersedes: None,
origin_brain: Some(origin_brain.clone()),
origin_claim: Some(source_slug.clone()),
promoted_by: Some(input.agent_id.clone()),
promotion_reason: Some(input.reason.clone()),
promoted_to: None,
evidence_unverifiable: reanchor.evidence_unverifiable,
soul_source: None,
};
let write = crate::light_author_handlers::write_light_memory_superseding(
&mut promoted_input,
&medulla_path,
medulla_runtime_root,
)?;
if let SupersessionOutcome::WouldDowngrade { reason } = write {
return Err(M1ndError::InvalidParams {
tool: "promote".into(),
detail: format!(
"promotion refused ({reason}): a stronger medulla claim '{medulla_slug}' is already \
live — a weaker re-promotion is bounced (the shared doctrine keeps its strongest \
form). Supersede it in its home brain to a higher state/confidence first."
),
});
}
let promoted_to_stamp = format!("medulla@{medulla_slug}@{}", now_ms());
stamp_witness(
&source_path,
&source_slug,
source_store_dir,
&promoted_to_stamp,
)?;
let medulla_claim_count = count_live_claims(medulla_store_dir);
Ok(PromoteOutcome {
medulla_path,
witness_path: source_path,
medulla_slug,
origin_brain,
origin_qualified: reanchor.origin_qualified,
evidence_unverifiable: reanchor.evidence_unverifiable,
medulla_claim_count,
soft_cap: MEDULLA_SOFT_CAP,
})
}
fn stamp_witness(witness_path: &Path, slug: &str, store_dir: &Path, stamp: &str) -> M1ndResult<()> {
let text = std::fs::read_to_string(witness_path).map_err(M1ndError::Io)?;
crate::light_author_handlers::archive_prior_as_outdated_in(store_dir, witness_path, slug)?;
let stamped = insert_or_replace_frontmatter(&text, "Promoted-To", stamp);
crate::light_author_handlers::write_atomic_pub(witness_path, &stamped)?;
Ok(())
}
fn insert_or_replace_frontmatter(text: &str, key: &str, value: &str) -> String {
let key_prefix = format!("{key}:");
let new_line = format!("{key}: {value}");
let mut out = String::with_capacity(text.len() + new_line.len() + 1);
let mut in_frontmatter = false;
let mut seen_open = false;
let mut replaced = false;
let mut inserted = false;
for line in text.lines() {
let trimmed = line.trim();
if trimmed == "---" {
if !seen_open {
seen_open = true;
in_frontmatter = true;
out.push_str(line);
out.push('\n');
continue;
} else if in_frontmatter {
if !replaced && !inserted {
out.push_str(&new_line);
out.push('\n');
inserted = true;
}
in_frontmatter = false;
out.push_str(line);
out.push('\n');
continue;
}
}
if in_frontmatter && trimmed.starts_with(&key_prefix) {
out.push_str(&new_line);
out.push('\n');
replaced = true;
continue;
}
out.push_str(line);
out.push('\n');
}
out
}
fn count_live_claims(store_dir: &Path) -> usize {
std::fs::read_dir(store_dir)
.into_iter()
.flatten()
.flatten()
.filter(|e| {
let name = e.file_name();
let name = name.to_string_lossy();
!name.starts_with('.') && name.ends_with(".light.md") && e.path().is_file()
})
.count()
}
pub fn promote_response(input: &PromoteInput, outcome: &PromoteOutcome) -> Value {
let over_cap = outcome.medulla_claim_count > outcome.soft_cap;
let mut resp = json!({
"ok": true,
"schema": "m1nd-promote-v0",
"promoted": true,
"medulla_path": outcome.medulla_path.to_string_lossy(),
"medulla_slug": outcome.medulla_slug,
"witness_path": outcome.witness_path.to_string_lossy(),
"origin_brain": outcome.origin_brain,
"promoted_by": input.agent_id,
"promotion_reason": input.reason,
"evidence": {
"origin_qualified": outcome.origin_qualified,
"evidence_unverifiable": outcome.evidence_unverifiable,
"note": if outcome.origin_qualified {
"evidence paths were origin-qualified (<origin_root>#<path>) — freshness delegates to the origin brain (C8.2 channel a)"
} else if outcome.evidence_unverifiable {
"the claim is stamped evidence_unverifiable — no resolvable origin root to qualify against; it never reads fresher than it can prove (C8.2 channel b)"
} else {
"the claim carried no code evidence — declared tissue, nothing to re-anchor"
},
},
"medulla_claim_count": outcome.medulla_claim_count,
"soft_cap": outcome.soft_cap,
"note": "promotion elevates, never moves: the project witness stays in place stamped Promoted-To; the medulla copy carries the full origin chain (Origin-Brain, Origin-Claim, Promoted-By, Promotion-Reason). Demotion (learn wrong / consolidation on the medulla copy) un-shares and never touches the witness.",
});
if over_cap {
resp["cap_warning"] = json!(format!(
"the medulla now holds {} claims — over the soft cap of {} (TT §6): run the consolidation pass to merge/supersede before it drifts from an index into a warehouse.",
outcome.medulla_claim_count, outcome.soft_cap
));
}
resp
}
#[cfg(test)]
mod tests {
use super::*;
fn claim(label: &str, evidence: Vec<&str>) -> LightClaim {
LightClaim {
label: label.into(),
text: Some(format!("{label} claim body.")),
kind: Some("entity".into()),
confidence: Some("0.9".into()),
ambiguity: None,
evidence: evidence.into_iter().map(String::from).collect(),
depends_on: vec![],
}
}
#[test]
fn c83_gate_allows_verified() {
let fm = ClaimFrontmatter {
state: Some("verified".into()),
..Default::default()
};
assert!(evidence_class_gate(&fm).is_ok());
}
#[test]
fn c83_gate_allows_founder_sourced() {
let fm = ClaimFrontmatter {
state: Some("authored".into()),
source_agent: Some("human:maintainer".into()),
..Default::default()
};
assert!(evidence_class_gate(&fm).is_ok());
}
#[test]
fn c83_gate_refuses_unverified_maker_claim() {
let fm = ClaimFrontmatter {
state: Some("authored".into()),
source_agent: Some("codex:maker".into()),
..Default::default()
};
let err = evidence_class_gate(&fm).unwrap_err();
assert!(err.contains("C8.3"), "gate reason must cite C8.3: {err}");
}
#[test]
fn hygiene_floor_passes_clean_claim() {
assert!(hygiene_floor("A perfectly clean doctrine claim about routing.").is_ok());
}
#[test]
fn hygiene_floor_refuses_secret() {
assert!(hygiene_floor("token: ghp_ABCDEFGHIJKLMNOP see auth").is_err());
assert!(hygiene_floor("-----BEGIN RSA PRIVATE KEY-----").is_err());
}
#[test]
fn hygiene_floor_refuses_conflict_marker() {
assert!(hygiene_floor("line\n<<<<<<< HEAD\nx\n=======\ny\n>>>>>>> other").is_err());
}
#[test]
fn reanchor_origin_qualifies_code_evidence() {
let claims = vec![claim("Router", vec!["src/router.rs", "src/lib.rs"])];
let r = reanchor_evidence(&claims, Some("/path/to/repo"));
assert!(r.origin_qualified, "code evidence must origin-qualify");
assert!(!r.evidence_unverifiable);
assert_eq!(r.claims[0].evidence[0], "/path/to/repo#src/router.rs");
assert_eq!(r.claims[0].evidence[1], "/path/to/repo#src/lib.rs");
}
#[test]
fn reanchor_marks_unverifiable_without_origin() {
let claims = vec![claim("Router", vec!["src/router.rs"])];
let r = reanchor_evidence(&claims, Some("medulla"));
assert!(!r.origin_qualified);
assert!(
r.evidence_unverifiable,
"evidence with no origin root must be marked unverifiable"
);
assert_eq!(r.claims[0].evidence[0], "src/router.rs");
}
#[test]
fn reanchor_no_evidence_is_neither() {
let claims = vec![claim("Doctrine", vec![])];
let r = reanchor_evidence(&claims, Some("/path/to/repo"));
assert!(!r.origin_qualified);
assert!(
!r.evidence_unverifiable,
"a claim with no evidence was never verified tissue — nothing to re-anchor"
);
}
#[test]
fn reanchor_is_idempotent_for_already_qualified() {
let mut c = claim("Router", vec![]);
c.evidence = vec!["/path/to/repo#src/router.rs".into()];
let r = reanchor_evidence(&[c], Some("/path/to/repo"));
assert_eq!(
r.claims[0].evidence[0], "/path/to/repo#src/router.rs",
"already-qualified evidence must not be double-prefixed"
);
}
#[test]
fn parse_reads_frontmatter_and_claims() {
let doc = "---\nProtocol: L1GHT/1.0\nNode: Router\nState: verified\nSource-Agent: codex:maker\nOrigin-Brain: /path/to/repo\n---\n\n# Router\n\n## Routing\n\nThe router dispatches by caller root.\n\n[⍂ entity: Router]\n[𝔻 confidence: 0.9]\n[𝔻 evidence: src/router.rs]\n";
let p = parse_light_claim(doc);
assert_eq!(p.frontmatter.node.as_deref(), Some("Router"));
assert_eq!(p.frontmatter.state.as_deref(), Some("verified"));
assert_eq!(p.frontmatter.source_agent.as_deref(), Some("codex:maker"));
assert_eq!(p.frontmatter.origin_brain.as_deref(), Some("/path/to/repo"));
assert_eq!(p.claims.len(), 1);
assert_eq!(p.claims[0].label, "Router");
assert_eq!(p.claims[0].confidence.as_deref(), Some("0.9"));
assert_eq!(p.claims[0].evidence, vec!["src/router.rs".to_string()]);
assert_eq!(
p.claims[0].text.as_deref(),
Some("The router dispatches by caller root.")
);
}
#[test]
fn insert_frontmatter_adds_before_closing_fence() {
let doc = "---\nNode: X\nState: verified\n---\n\n# X\n";
let out = insert_or_replace_frontmatter(doc, "Promoted-To", "medulla@x@123");
assert!(out.contains("Promoted-To: medulla@x@123"));
let fence_close = out.find("\n---\n\n# X").expect("closing fence intact");
let stamp = out.find("Promoted-To").expect("stamp present");
assert!(stamp < fence_close, "stamp must be inside the frontmatter");
}
#[test]
fn insert_frontmatter_replaces_existing() {
let doc = "---\nNode: X\nPromoted-To: old\n---\n";
let out = insert_or_replace_frontmatter(doc, "Promoted-To", "new");
assert!(out.contains("Promoted-To: new"));
assert!(!out.contains("Promoted-To: old"));
}
}