use crate::audit_handlers::resolve_git_root_from_state;
use crate::protocol::layers::{SoulCheckInput, SoulReadInput};
use crate::session::SessionState;
use crate::util::now_ms;
use m1nd_core::error::{M1ndError, M1ndResult};
use serde_json::json;
use std::path::{Path, PathBuf};
use std::process::Command;
const SOUL_DISCOVERY: &[&str] = &["docs/PATHOS.md", "PATHOS.md"];
const DECLARED_SECTIONS: &[&str] = &[
"north star",
"pathos",
"operating doctrine",
"do not do",
"open questions",
"proof standard",
];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Tissue {
Verifiable,
Declared,
}
impl Tissue {
fn as_str(self) -> &'static str {
match self {
Tissue::Verifiable => "verifiable",
Tissue::Declared => "declared",
}
}
}
fn tissue_for_section(section: &str) -> Tissue {
let lower = section.to_ascii_lowercase();
if DECLARED_SECTIONS.iter().any(|d| lower.contains(d)) {
Tissue::Declared
} else {
Tissue::Verifiable
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CheckClass {
Path,
LineHint,
Symbol,
Git,
Receipt,
Runtime,
Declared,
}
impl CheckClass {
fn as_str(self) -> &'static str {
match self {
CheckClass::Path => "path",
CheckClass::LineHint => "line-hint",
CheckClass::Symbol => "symbol",
CheckClass::Git => "git",
CheckClass::Receipt => "receipt",
CheckClass::Runtime => "runtime",
CheckClass::Declared => "declared",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SoulState {
VerifiedFresh,
EvidenceStale,
Superseded,
ReceiptRequired,
UnprovableNow,
Declared,
}
impl SoulState {
fn as_str(self) -> &'static str {
match self {
SoulState::VerifiedFresh => "verified-fresh",
SoulState::EvidenceStale => "evidence-stale",
SoulState::Superseded => "superseded",
SoulState::ReceiptRequired => "receipt-required",
SoulState::UnprovableNow => "unprovable-now",
SoulState::Declared => "declared",
}
}
}
#[derive(Clone, Debug)]
struct Anchor {
ref_text: String,
class: CheckClass,
line_hint: Option<(String, usize)>,
}
#[derive(Clone, Debug)]
struct SoulClaim {
section: String,
tissue: Tissue,
text: String,
anchors: Vec<Anchor>,
}
const AUTO_BEGIN: &str = "<!-- BEGIN:auto-";
const AUTO_END: &str = "<!-- END:auto-";
fn parse_soul(body: &str) -> Vec<SoulClaim> {
let mut claims = Vec::new();
let mut section = String::from("<preamble>");
let mut tissue = Tissue::Declared;
let mut in_auto = false;
let mut in_history = false;
for raw in body.lines() {
let line = raw.trim_end();
if line.contains(AUTO_BEGIN) {
in_auto = true;
continue;
}
if line.contains(AUTO_END) {
in_auto = false;
continue;
}
if in_auto {
continue;
}
if let Some(heading) = line.strip_prefix("## ") {
section = heading.trim().to_string();
tissue = tissue_for_section(§ion);
in_history = {
let l = section.to_ascii_lowercase();
l.contains("prior era") || l.contains("prior checkpoint")
};
continue;
}
if line.starts_with("### ") {
continue;
}
if in_history {
continue;
}
let text = line.trim();
if text.is_empty() || text == ">" {
continue;
}
let anchors = extract_anchors(text);
let is_bullet = text.starts_with("- ") || text.starts_with("* ");
if anchors.is_empty() && tissue == Tissue::Verifiable && !is_bullet {
continue;
}
let is_scaffold =
text.starts_with('>') || text.starts_with("---") || text.starts_with("<!--");
if is_scaffold {
continue;
}
claims.push(SoulClaim {
section: section.clone(),
tissue,
text: text.to_string(),
anchors,
});
}
claims
}
fn extract_anchors(text: &str) -> Vec<Anchor> {
let mut anchors = Vec::new();
for span in backtick_spans(text) {
if let Some(anchor) = classify_backtick(&span) {
anchors.push(anchor);
}
}
for token in text.split(|c: char| c.is_whitespace() || matches!(c, '(' | ')' | ',' | '.')) {
let t = token.trim();
if (is_version_tag(t) || is_pr_ref(t)) && !anchors.iter().any(|a| a.ref_text == t) {
anchors.push(Anchor {
ref_text: t.to_string(),
class: CheckClass::Git,
line_hint: None,
});
}
}
anchors
}
fn backtick_spans(text: &str) -> Vec<String> {
let mut spans = Vec::new();
let mut chars = text.chars().peekable();
let mut current: Option<String> = None;
while let Some(c) = chars.next() {
if c == '`' {
match current.take() {
Some(s) => spans.push(s),
None => current = Some(String::new()),
}
} else if let Some(buf) = current.as_mut() {
buf.push(c);
}
let _ = &mut chars;
}
spans
}
fn classify_backtick(span: &str) -> Option<Anchor> {
let s = span.trim();
if s.is_empty() {
return None;
}
if let Some((left, right)) = s.rsplit_once(':') {
if let Ok(line) = right.trim().parse::<usize>() {
if looks_like_path(left) {
return Some(Anchor {
ref_text: s.to_string(),
class: CheckClass::LineHint,
line_hint: Some((left.trim().to_string(), line)),
});
}
}
}
if looks_like_path(s) {
return Some(Anchor {
ref_text: s.to_string(),
class: CheckClass::Path,
line_hint: None,
});
}
if is_version_tag(s) || is_pr_ref(s) {
return Some(Anchor {
ref_text: s.to_string(),
class: CheckClass::Git,
line_hint: None,
});
}
if is_symbol(s) {
return Some(Anchor {
ref_text: s.to_string(),
class: CheckClass::Symbol,
line_hint: None,
});
}
None
}
fn looks_like_path(s: &str) -> bool {
let s = s.trim();
if s.is_empty() || s.contains(' ') {
return false;
}
if s.starts_with('~')
|| s.starts_with('$')
|| s.starts_with("./target")
|| s.starts_with('/')
|| s.contains("/target/")
|| s.starts_with("target/")
{
return false;
}
if s.starts_with('!') {
return false;
}
if s.starts_with('.') && !s.contains('/') {
if s.contains("::") {
return false;
}
let segs = s.trim_start_matches('.').split('.').count();
return segs == 1;
}
if s.contains('/') {
let last = s.rsplit('/').next().unwrap_or("");
return !last.is_empty();
}
false
}
fn is_version_tag(s: &str) -> bool {
let core = s.split('+').next().unwrap_or(s);
if let Some(rest) = core.strip_prefix('v') {
let parts: Vec<&str> = rest.split('.').collect();
return parts.len() == 3 && parts.iter().all(|p| p.chars().all(|c| c.is_ascii_digit()));
}
false
}
fn is_pr_ref(s: &str) -> bool {
s.strip_prefix('#')
.map(|n| !n.is_empty() && n.chars().all(|c| c.is_ascii_digit()))
.unwrap_or(false)
}
fn is_symbol(s: &str) -> bool {
if !s.contains("::") || s.contains(' ') || s.contains('/') {
return false;
}
!s.contains('(') && !s.contains('.') && s.len() < 80
}
struct AnchorVerdict {
state: SoulState,
reason: Option<&'static str>,
}
fn verify_anchor(
anchor: &Anchor,
repo_root: &Path,
state: &SessionState,
ran_symbol: &mut bool,
) -> AnchorVerdict {
match anchor.class {
CheckClass::Path => verify_path(&anchor.ref_text, repo_root),
CheckClass::LineHint => verify_line_hint(anchor, repo_root),
CheckClass::Symbol => {
*ran_symbol = true;
verify_symbol(&anchor.ref_text, state)
}
CheckClass::Git => verify_git(&anchor.ref_text, repo_root),
CheckClass::Receipt => AnchorVerdict {
state: SoulState::ReceiptRequired,
reason: None,
},
CheckClass::Runtime => AnchorVerdict {
state: SoulState::UnprovableNow,
reason: None,
},
CheckClass::Declared => AnchorVerdict {
state: SoulState::Declared,
reason: None,
},
}
}
fn verify_path(rel: &str, repo_root: &Path) -> AnchorVerdict {
let full = repo_root.join(rel);
if full.exists() {
AnchorVerdict {
state: SoulState::VerifiedFresh,
reason: None,
}
} else {
AnchorVerdict {
state: SoulState::EvidenceStale,
reason: Some("evidence_file_missing"),
}
}
}
fn verify_line_hint(anchor: &Anchor, repo_root: &Path) -> AnchorVerdict {
let Some((path, line)) = &anchor.line_hint else {
return verify_path(&anchor.ref_text, repo_root);
};
let full = repo_root.join(path);
let Ok(content) = std::fs::read_to_string(&full) else {
return AnchorVerdict {
state: SoulState::EvidenceStale,
reason: Some("evidence_file_missing"),
};
};
let total = content.lines().count();
if *line >= 1 && *line <= total {
AnchorVerdict {
state: SoulState::VerifiedFresh,
reason: None,
}
} else {
AnchorVerdict {
state: SoulState::EvidenceStale,
reason: Some("line_drift"),
}
}
}
fn verify_symbol(sym: &str, state: &SessionState) -> AnchorVerdict {
let (head, tail) = match sym.rsplit_once("::") {
Some((h, t)) => (h, t),
None => (sym, sym),
};
let graph = state.graph.read();
for (interned, &node_id) in &graph.id_to_node {
let ext_id = graph.strings.resolve(*interned);
let idx = node_id.as_usize();
let label = graph.strings.resolve(graph.nodes.label[idx]);
if label == tail && (ext_id.contains(head) || head == tail) {
return AnchorVerdict {
state: SoulState::VerifiedFresh,
reason: None,
};
}
}
AnchorVerdict {
state: SoulState::EvidenceStale,
reason: Some("evidence_changed"),
}
}
fn verify_git(git_ref: &str, repo_root: &Path) -> AnchorVerdict {
if is_version_tag(git_ref) {
let ok = git_ok(
repo_root,
&[
"rev-parse",
"-q",
"--verify",
&format!("refs/tags/{}", git_ref),
],
);
return git_verdict(ok);
}
if let Some(num) = git_ref.strip_prefix('#') {
let needle = format!("(#{})", num);
if let Some(log) = git_read(repo_root, &["log", "--oneline", "-n", "400"]) {
let ok = log.lines().any(|l| l.contains(&needle));
return git_verdict(ok);
}
return AnchorVerdict {
state: SoulState::UnprovableNow,
reason: None,
};
}
AnchorVerdict {
state: SoulState::UnprovableNow,
reason: None,
}
}
fn git_verdict(ok: bool) -> AnchorVerdict {
if ok {
AnchorVerdict {
state: SoulState::VerifiedFresh,
reason: None,
}
} else {
AnchorVerdict {
state: SoulState::EvidenceStale,
reason: Some("unanchored"),
}
}
}
fn git_ok(root: &Path, args: &[&str]) -> bool {
Command::new("git")
.current_dir(root)
.args(args)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn git_read(root: &Path, args: &[&str]) -> Option<String> {
let out = Command::new("git")
.current_dir(root)
.args(args)
.output()
.ok()?;
if !out.status.success() {
return None;
}
Some(String::from_utf8_lossy(&out.stdout).into_owned())
}
fn roll_claim_state(
claim: &SoulClaim,
repo_root: &Path,
state: &SessionState,
ran_symbol: &mut bool,
) -> (SoulState, Option<&'static str>) {
if claim.tissue == Tissue::Declared {
return (SoulState::Declared, None);
}
if claim.anchors.is_empty() {
return (SoulState::EvidenceStale, Some("unanchored"));
}
let mut worst: Option<(SoulState, Option<&'static str>)> = None;
for anchor in &claim.anchors {
let v = verify_anchor(anchor, repo_root, state, ran_symbol);
worst = Some(match worst.take() {
None => (v.state, v.reason),
Some(prev) => pick_worse(prev, (v.state, v.reason)),
});
}
worst.unwrap_or((SoulState::VerifiedFresh, None))
}
fn pick_worse(
a: (SoulState, Option<&'static str>),
b: (SoulState, Option<&'static str>),
) -> (SoulState, Option<&'static str>) {
if rank(a.0) >= rank(b.0) {
a
} else {
b
}
}
fn rank(s: SoulState) -> u8 {
match s {
SoulState::EvidenceStale => 5,
SoulState::Superseded => 4,
SoulState::UnprovableNow => 3,
SoulState::ReceiptRequired => 2,
SoulState::VerifiedFresh => 1,
SoulState::Declared => 0,
}
}
fn consistency_findings(claims: &[SoulClaim]) -> Vec<serde_json::Value> {
use std::collections::HashMap;
let mut seen: HashMap<&'static str, Vec<(u64, String)>> = HashMap::new();
const TRACKED: &[&str] = &["battery"];
for claim in claims {
let lower = claim.text.to_ascii_lowercase();
for kw in TRACKED {
if lower.contains(kw) {
for num in extract_small_ints(&claim.text) {
if (20..=99).contains(&num) {
let entry = seen.entry(kw).or_default();
if !entry.iter().any(|(n, _)| *n == num) {
entry.push((num, claim.text.clone()));
}
}
}
}
}
}
let mut findings = Vec::new();
for (kw, values) in seen {
if values.len() > 1 {
let nums: Vec<String> = values.iter().map(|(n, _)| n.to_string()).collect();
findings.push(json!({
"quantity": kw,
"reason": "contradicted",
"values": nums,
"detail": format!(
"the soul asserts {} as {} in different places — intra-soul disagreement",
kw,
nums.join(" and ")
),
}));
}
}
findings
}
fn superseded_claim_indices(claims: &[SoulClaim]) -> std::collections::HashSet<usize> {
use std::collections::{HashMap, HashSet};
let mut latest_for_anchor: HashMap<&str, usize> = HashMap::new();
for (idx, claim) in claims.iter().enumerate() {
for anchor in &claim.anchors {
latest_for_anchor
.entry(anchor.ref_text.as_str())
.and_modify(|slot| {
if idx > *slot {
*slot = idx;
}
})
.or_insert(idx);
}
}
let mut superseded = HashSet::new();
for (idx, claim) in claims.iter().enumerate() {
if claim.anchors.is_empty() {
continue;
}
let mut latest_of_some = false;
let mut shadowed_by_later = false;
for anchor in &claim.anchors {
match latest_for_anchor.get(anchor.ref_text.as_str()) {
Some(&latest) if latest == idx => latest_of_some = true,
Some(&latest) if latest > idx => shadowed_by_later = true,
_ => {}
}
}
if shadowed_by_later && !latest_of_some {
superseded.insert(idx);
}
}
superseded
}
fn extract_small_ints(text: &str) -> Vec<u64> {
let mut out = Vec::new();
let mut cur = String::new();
for c in text.chars() {
if c.is_ascii_digit() {
cur.push(c);
} else {
if let Ok(n) = cur.parse::<u64>() {
out.push(n);
}
cur.clear();
}
}
if let Ok(n) = cur.parse::<u64>() {
out.push(n);
}
out
}
fn resolve_soul_path(
state: &SessionState,
explicit: Option<&str>,
) -> M1ndResult<(PathBuf, PathBuf)> {
let repo_root = resolve_git_root_from_state(state).ok_or_else(|| M1ndError::InvalidParams {
tool: "soul_check".into(),
detail: "no git repo root resolvable from the session — the soul travels with git".into(),
})?;
if let Some(p) = explicit {
let candidate = {
let raw = Path::new(p);
if raw.is_absolute() {
raw.to_path_buf()
} else {
repo_root.join(raw)
}
};
if candidate.exists() {
return Ok((repo_root, candidate));
}
return Err(M1ndError::InvalidParams {
tool: "soul_check".into(),
detail: format!("soul_path '{}' does not exist under {:?}", p, repo_root),
});
}
for rel in SOUL_DISCOVERY {
let candidate = repo_root.join(rel);
if candidate.exists() {
return Ok((repo_root, candidate));
}
}
Err(M1ndError::InvalidParams {
tool: "soul_check".into(),
detail: format!(
"no soul document found (looked for {:?} under {:?}) — the pathos skill births one",
SOUL_DISCOVERY, repo_root
),
})
}
fn short_sha(repo_root: &Path) -> String {
git_read(repo_root, &["rev-parse", "--short", "HEAD"])
.map(|s| s.trim().to_string())
.unwrap_or_else(|| "unknown".to_string())
}
pub fn handle_soul_check(
state: &mut SessionState,
input: SoulCheckInput,
) -> M1ndResult<serde_json::Value> {
if let Some(report) = input.verify_curator_report {
return verify_curator_report(&input.agent_id, &report);
}
let (repo_root, soul_path) = resolve_soul_path(state, input.soul_path.as_deref())?;
let body = std::fs::read_to_string(&soul_path).map_err(|e| M1ndError::InvalidParams {
tool: "soul_check".into(),
detail: format!("cannot read soul at {:?}: {}", soul_path, e),
})?;
let claims = parse_soul(&body);
let superseded_indices = superseded_claim_indices(&claims);
let mut ran_symbol = false;
let mut by_state = [0usize; 6]; let mut verifiable = 0usize;
let mut declared = 0usize;
let mut unanchored = 0usize;
let mut stale_rows: Vec<serde_json::Value> = Vec::new();
let mut superseded_rows: Vec<serde_json::Value> = Vec::new();
for (idx, claim) in claims.iter().enumerate() {
if claim.tissue == Tissue::Declared {
declared += 1;
} else {
verifiable += 1;
}
let (st, reason) = if superseded_indices.contains(&idx) {
(SoulState::Superseded, Some("superseded_by_later_claim"))
} else {
roll_claim_state(claim, &repo_root, state, &mut ran_symbol)
};
let bucket = match st {
SoulState::VerifiedFresh => 0,
SoulState::EvidenceStale => 1,
SoulState::Superseded => 2,
SoulState::ReceiptRequired => 3,
SoulState::UnprovableNow => 4,
SoulState::Declared => 5,
};
by_state[bucket] += 1;
if reason == Some("unanchored") {
unanchored += 1;
}
if st == SoulState::Superseded {
let anchor_txt = claim
.anchors
.first()
.map(|a| a.ref_text.clone())
.unwrap_or_else(|| "<none>".into());
superseded_rows.push(json!({
"claim": truncate(&claim.text, 160),
"section": claim.section,
"reason": reason.unwrap_or("superseded_by_later_claim"),
"anchor": anchor_txt,
}));
}
if st == SoulState::EvidenceStale {
let anchor_txt = claim
.anchors
.first()
.map(|a| a.ref_text.clone())
.unwrap_or_else(|| "<none>".into());
stale_rows.push(json!({
"claim": truncate(&claim.text, 160),
"section": claim.section,
"reason": reason.unwrap_or("evidence_changed"),
"anchor": anchor_txt,
"class": claim.anchors.first().map(|a| a.class.as_str()).unwrap_or("none"),
}));
}
}
let consistency = consistency_findings(&claims);
for finding in &consistency {
by_state[1] += 1; stale_rows.push(finding.clone());
}
let mut checks_skipped: Vec<&'static str> = Vec::new();
if !ran_symbol {
checks_skipped.push("symbol (no symbol anchors present)");
}
if by_state[3] > 0 {
checks_skipped.push("receipt (execution not run — receipt-required)");
}
if by_state[4] > 0 {
checks_skipped.push("runtime (live owner not probed — unprovable-now)");
}
let fresh = by_state[0];
let stale = by_state[1];
let superseded = by_state[2];
let receipt = by_state[3];
let unprovable = by_state[4];
let repo_sha = short_sha(&repo_root);
let checked_at = now_ms();
let date = ymd(checked_at);
let receipt_line = format!(
"soul: checked {date} @{repo_sha} — {fresh} fresh · {stale} stale · {} priced · declared tissue intact",
receipt + unprovable
);
let soul_lag = compute_soul_lag(&repo_root, &soul_path);
let out = json!({
"schema": "m1nd-soul-check-v0",
"soul_path": display_rel(&repo_root, &soul_path),
"checked_at_ms": checked_at,
"repo_sha": repo_sha,
"checked_by": input.agent_id,
"claims": {
"total": claims.len(),
"verifiable": verifiable,
"declared": declared,
"unanchored_in_verifiable": unanchored,
},
"by_state": {
"verified_fresh": fresh,
"evidence_stale": stale,
"superseded": superseded,
"receipt_required": receipt,
"unprovable_now": unprovable,
"declared": by_state[5],
},
"stale": stale_rows,
"superseded_claims": superseded_rows,
"consistency_findings": consistency,
"checks_skipped": checks_skipped,
"receipt_line": receipt_line,
"soul_lag": soul_lag,
"_soul": {
"read_only": true,
"note": "S0 read-only: no store touched, no document edited (SOUL-PRD §9).",
"two_tissues": "declared tissue is UNPROVABLE-but-curated — never machine-verified (SOUL-INV-5).",
},
});
Ok(out)
}
fn compute_soul_lag(repo_root: &Path, soul_path: &Path) -> serde_json::Value {
let rel = display_rel(repo_root, soul_path);
let last_touch = git_read(
repo_root,
&["log", "-n", "1", "--format=%h", "--", rel.as_str()],
)
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let behind = last_touch.as_ref().and_then(|sha| {
git_read(
repo_root,
&["rev-list", "--count", &format!("{}..HEAD", sha)],
)
.and_then(|s| s.trim().parse::<u64>().ok())
});
json!({
"commits_behind_head": behind,
"last_soul_touch": last_touch,
})
}
fn verify_curator_report(
grader_id: &str,
report: &serde_json::Value,
) -> M1ndResult<serde_json::Value> {
let curated_by = report
.get("curated_by")
.and_then(|v| v.as_str())
.unwrap_or("");
let mut violations: Vec<String> = Vec::new();
let seat_ok = !curated_by.is_empty() && curated_by != grader_id;
if curated_by.is_empty() {
violations.push("report has no `curated_by` provenance (etiquette-by-provenance)".into());
} else if curated_by == grader_id {
violations.push(format!(
"seat violation: the grader ('{}') is the curator — §C8.4 requires a DIFFERENT session/agent",
grader_id
));
}
if let Some(pruned) = report.get("pruned").and_then(|v| v.as_array()) {
for (i, p) in pruned.iter().enumerate() {
let has_where = p
.get("where_it_went")
.and_then(|v| v.as_str())
.map(|s| !s.is_empty())
.unwrap_or(false);
let has_why = p
.get("why")
.and_then(|v| v.as_str())
.map(|s| !s.is_empty())
.unwrap_or(false);
if !has_where || !has_why {
violations.push(format!(
"prune #{} is silent: every removal needs a `why` and a `where_it_went` (SOUL-INV-2)",
i
));
}
}
}
if report.get("still_stale").is_none() {
violations.push("report is missing the `still_stale` honesty valve (SOUL-PRD §5.3)".into());
}
if let Some(pruned) = report.get("pruned").and_then(|v| v.as_array()) {
for (i, p) in pruned.iter().enumerate() {
let tissue = p.get("tissue").and_then(|v| v.as_str()).unwrap_or("");
let proposed_only = p.get("proposed").and_then(|v| v.as_bool()).unwrap_or(false);
if tissue == "declared" && !proposed_only {
violations.push(format!(
"prune #{} removes DECLARED tissue without `proposed: true` — declared tissue is proposed-only (SOUL-INV-5)",
i
));
}
}
}
let passed = seat_ok && violations.is_empty();
Ok(json!({
"schema": "m1nd-soul-curator-seatcheck-v0",
"passed": passed,
"grader": grader_id,
"curated_by": curated_by,
"seat_independent": seat_ok,
"violations": violations,
"law": "ORGANISM §C8.4 — curator output is seat-verified: grader ≠ author; never-silent-prune; declared-tissue lock; still_stale honesty valve present.",
}))
}
pub fn handle_soul_read(
state: &mut SessionState,
input: SoulReadInput,
) -> M1ndResult<serde_json::Value> {
let (repo_root, soul_path) = resolve_soul_path(state, input.soul_path.as_deref())?;
let body = std::fs::read_to_string(&soul_path).map_err(|e| M1ndError::InvalidParams {
tool: "soul_read".into(),
detail: format!("cannot read soul at {:?}: {}", soul_path, e),
})?;
let content = if let Some(section) = input.section.as_deref() {
extract_section(&body, section).ok_or_else(|| M1ndError::InvalidParams {
tool: "soul_read".into(),
detail: format!("no section matching '{}' in the soul", section),
})?
} else {
body.clone()
};
let headline = extract_headline(&body);
Ok(json!({
"schema": "m1nd-soul-read-v0",
"soul_path": display_rel(&repo_root, &soul_path),
"headline": headline,
"section": input.section,
"content": content,
"hint": "run soul_check for the freshness receipt (how much to trust this handoff).",
}))
}
fn extract_section(body: &str, wanted: &str) -> Option<String> {
let wanted = wanted.to_ascii_lowercase();
let mut out: Option<Vec<String>> = None;
for line in body.lines() {
if let Some(h) = line.strip_prefix("## ") {
if h.to_ascii_lowercase().contains(&wanted) {
out = Some(vec![line.to_string()]);
continue;
} else if out.is_some() {
break; }
}
if let Some(buf) = out.as_mut() {
buf.push(line.to_string());
}
}
out.map(|lines| lines.join("\n"))
}
fn extract_headline(body: &str) -> String {
for line in body.lines() {
let t = line.trim();
if let Some(h) = t.strip_prefix("# ") {
return truncate(h.trim(), 120);
}
}
"soul".to_string()
}
fn display_rel(repo_root: &Path, path: &Path) -> String {
path.strip_prefix(repo_root)
.unwrap_or(path)
.to_string_lossy()
.replace('\\', "/")
}
fn truncate(s: &str, max: usize) -> String {
if s.chars().count() <= max {
s.to_string()
} else {
let truncated: String = s.chars().take(max.saturating_sub(1)).collect();
format!("{}…", truncated)
}
}
fn ymd(ms: u64) -> String {
let secs = (ms / 1000) as i64;
let days = secs.div_euclid(86_400);
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
format!("{:04}-{:02}-{:02}", y, m, d)
}
#[cfg(test)]
mod tests {
use super::*;
fn claim_with_anchors(text: &str, anchors: &[&str]) -> SoulClaim {
SoulClaim {
section: "State".into(),
tissue: Tissue::Verifiable,
text: text.into(),
anchors: anchors
.iter()
.map(|a| Anchor {
ref_text: (*a).into(),
class: CheckClass::Path,
line_hint: None,
})
.collect(),
}
}
#[test]
fn oldest_duplicate_anchor_claim_is_superseded() {
let claims = vec![
claim_with_anchors("auth was validated here", &["src/auth.rs"]),
claim_with_anchors("auth is now validated differently", &["src/auth.rs"]),
];
let superseded = superseded_claim_indices(&claims);
assert!(
superseded.contains(&0),
"the older claim (index 0) on the shared anchor must be superseded"
);
assert!(
!superseded.contains(&1),
"the newer claim (index 1) is the current word and survives"
);
}
#[test]
fn claim_latest_on_one_anchor_survives() {
let claims = vec![
claim_with_anchors("older single", &["a.rs"]),
claim_with_anchors("spans two anchors", &["a.rs", "b.rs"]),
];
let superseded = superseded_claim_indices(&claims);
assert!(superseded.contains(&0));
assert!(!superseded.contains(&1));
}
#[test]
fn distinct_anchors_are_never_superseded() {
let claims = vec![
claim_with_anchors("one", &["x.rs"]),
claim_with_anchors("two", &["y.rs"]),
claim_with_anchors("three", &[]),
];
assert!(superseded_claim_indices(&claims).is_empty());
}
#[test]
fn parse_soul_marks_older_shared_anchor_claim_superseded() {
let body = "\
## State
The parser lives in `src/parser.rs` and did the old thing.
The parser in `src/parser.rs` now does the new thing.
";
let claims = parse_soul(body);
let superseded = superseded_claim_indices(&claims);
assert_eq!(
superseded.len(),
1,
"exactly one (the older) of two same-anchor claims is superseded, claims={:#?}",
claims
);
let superseded_idx = *superseded.iter().next().unwrap();
let survivor_idx = claims
.iter()
.enumerate()
.filter(|(_, c)| c.anchors.iter().any(|a| a.ref_text == "src/parser.rs"))
.map(|(i, _)| i)
.max()
.unwrap();
assert!(
superseded_idx < survivor_idx,
"the superseded claim precedes the surviving one"
);
}
}