use std::fs;
use std::path::Path;
use aristo_core::index::{
AnnotationId, AssumeEntry, BindingState, IdNamespace, IndexEntry, IndexFile, IntentEntry,
ParentLink, Status, VerifyLevel, VerifyMethod,
};
use crate::commands::index::workspace_or_error;
use crate::commands::show::read_index;
use crate::preflight::{emit_advisory_if_stale, freshness_check};
use crate::{CliError, CliResult, Workspace};
pub(crate) fn run(
summary: bool,
include_status: bool,
check: bool,
include_graph: bool,
) -> CliResult<()> {
let ws = workspace_or_error()?;
emit_advisory_if_stale(&freshness_check(&ws));
let index = read_index(&ws.index_path())?;
if check {
return run_check(&ws, &index, include_status);
}
if summary || include_graph {
run_summary(&ws, &index)?;
if include_graph {
append_graph_to_summary(&ws, &index)?;
}
return Ok(());
}
run_per_annotation(&ws, &index, include_status)
}
#[aristo::intent(
"`aristo doc --include-graph` appends the rendered annotation graph \
(as a fenced ```mermaid block) to `_summary.md` AFTER the \
summary has been written. Order matters: appending after means \
a re-run with `--include-graph` produces the same file regardless \
of whether the prior run had the flag. A refactor that prepended \
or inserted-in-middle would make the output dependent on prior \
state, which makes `--check` brittle.",
verify = "neural",
id = "doc_include_graph_appends_idempotently"
)]
fn append_graph_to_summary(ws: &Workspace, index: &IndexFile) -> CliResult<()> {
let summary_path = ws.root.join(".aristo").join("doc").join("_summary.md");
let graph = crate::commands::graph::model::build_with_axis(
index,
crate::commands::graph::ColorAxis::Verify,
);
let mermaid = crate::commands::graph::mermaid::render(&graph);
let existing = fs::read_to_string(&summary_path).unwrap_or_default();
let mut out = existing;
if !out.ends_with('\n') {
out.push('\n');
}
out.push('\n');
out.push_str("## Annotation graph\n\n");
out.push_str(&mermaid);
fs::write(&summary_path, &out).map_err(CliError::Io)?;
println!("→ Appending annotation graph to .aristo/doc/_summary.md");
println!(
" • {} nodes, {} edges (Mermaid, embedded inline)",
graph.nodes.len(),
graph.edges.len()
);
Ok(())
}
#[aristo::intent(
"`aristo doc --check` is a CI gate: it MUST NOT write to \
`.aristo/doc/` under any circumstance — its job is to detect \
drift so CI can block a merge that has stale doc artifacts. A \
regression that wrote during --check would silently fix the \
thing CI was supposed to catch.",
verify = "neural",
id = "doc_check_never_writes"
)]
fn run_check(ws: &Workspace, index: &IndexFile, include_status: bool) -> CliResult<()> {
let doc_dir = ws.root.join(".aristo").join("doc");
println!("→ Reading .aristo/index.toml … ok");
println!("→ Computing expected per-annotation markdown …");
println!("→ Comparing against .aristo/doc/ on disk …");
let mut drift: Vec<String> = Vec::new();
for (id, entry) in &index.entries {
let path = doc_dir.join(format!("{}.md", id_safe(id)));
let rendered = render_annotation_md(id, entry, include_status);
let on_disk = fs::read_to_string(&path).unwrap_or_default();
if on_disk != rendered {
drift.push(id_safe(id));
}
}
if drift.is_empty() {
println!();
println!("ok: doc artifacts are in sync with the index.");
return Ok(());
}
for d in &drift {
println!(" • Out of sync: .aristo/doc/{d}.md");
println!(" (text in index does not match rendered markdown)");
}
println!();
let n = drift.len();
let plural = if n == 1 { "" } else { "s" };
Err(CliError::Other {
message: format!(
"{n} doc artifact{plural} out of sync with the index.\n\
\x20 Run `aristo doc` locally and commit the result."
),
exit_code: 1,
})
}
#[aristo::intent(
"`aristo doc` writes each annotation to .aristo/doc/<id-safe>.md \
where `<id-safe>` substitutes `:` with `__`. Same convention as \
`.proof` and `.critique` files so users have one mental model for \
id↔filename mapping across the SDK. A regression that picks a \
different escape (or uses the raw id with `:`) would create \
platform-specific filename failures (`:` is illegal on Windows / \
macOS HFS+) AND silently break the slice-30 proc-macro that \
reads these files via `include_str!`.",
verify = "neural",
id = "doc_per_annotation_filename_uses_id_safe"
)]
#[aristo::intent(
"`aristo doc` output shape differs by first-run-vs-incremental: \
first run (empty .aristo/doc/) prints per-file `• Wrote:` lines, \
a `(N files written, 0 unchanged)` count, and the `Next steps` \
onboarding footer; subsequent runs (any pre-existing file) print \
`• Updated:`/`• Unchanged:` lines and a compressed \
`ok: doc artifacts updated. (M written, N unchanged)` summary \
with no onboarding footer. The pivot is whether the doc dir was \
empty before the run, not whether any file was unchanged this \
time — a regression that switched to the count-based check \
would emit onboarding footers on every run that happens to \
write all files (e.g. a schema upgrade that touches every MD).",
verify = "neural",
id = "doc_output_shape_pivots_on_empty_doc_dir_not_per_run_counts"
)]
fn run_per_annotation(ws: &Workspace, index: &IndexFile, include_status: bool) -> CliResult<()> {
let doc_dir = ws.root.join(".aristo").join("doc");
let is_first_run = doc_dir
.read_dir()
.map(|mut d| d.next().is_none())
.unwrap_or(true);
fs::create_dir_all(&doc_dir).map_err(CliError::Io)?;
let counts = Counts::from(index);
println!();
if include_status {
println!(
"→ Reading .aristo/index.toml … ok ({} annotations)",
counts.total,
);
println!("→ Including current B5b verification status in rendered docs.");
println!("→ Generating per-annotation markdown to .aristo/doc/ …");
} else if is_first_run {
println!(
"→ Reading .aristo/index.toml … ok ({} annotations: {} intent, {} assume)",
counts.total, counts.intent, counts.assume,
);
println!("→ Generating per-annotation markdown to .aristo/doc/ …");
} else {
println!(
"→ Reading .aristo/index.toml … ok ({} annotations)",
counts.total,
);
println!("→ Generating per-annotation markdown …");
}
let mut new_count = 0usize;
let mut updated_count = 0usize;
let mut unchanged_count = 0usize;
for (id, entry) in &index.entries {
let path = doc_dir.join(format!("{}.md", id_safe(id)));
let already_exists = path.exists();
let rendered = render_annotation_md(id, entry, include_status);
if file_unchanged(&path, &rendered) {
unchanged_count += 1;
continue;
}
fs::write(&path, &rendered).map_err(CliError::Io)?;
if include_status {
} else if !already_exists {
println!(" • Wrote: .aristo/doc/{}.md", id_safe(id));
new_count += 1;
} else {
println!(
" • Updated: .aristo/doc/{}.md (text changed)",
id_safe(id)
);
updated_count += 1;
}
}
let written = new_count + updated_count;
if include_status {
let total_writes = index.entries.len() - unchanged_count;
println!(" ({total_writes} files written, including status blocks)");
} else if is_first_run {
println!(" ({written} files written, {unchanged_count} unchanged)");
} else if unchanged_count > 0 {
println!(" • Unchanged: {unchanged_count} files");
}
println!();
if include_status {
println!("ok: doc artifacts updated.");
println!();
println!("ℹ Status is a build-time fact and will become stale as code evolves.");
println!(" Re-run `aristo doc --include-status` to refresh, or omit `--include-status`");
println!(" for purely static rendering.");
} else if is_first_run {
println!("ok: doc artifacts updated.");
println!();
println!("Next steps:");
println!(" • Enable the `aristo_doc` cargo feature in your Cargo.toml:");
println!(" [features]");
println!(" default = [\"aristo_doc\"]");
println!(" • Or run `cargo doc --features aristo_doc` to render docs with annotations.");
println!(" • Optional: add `#[doc = include_str!(\".aristo/doc/_summary.md\")]`");
println!(" above your crate's `//!` doc to render the project-level summary.");
} else {
println!("ok: doc artifacts updated. ({written} written, {unchanged_count} unchanged)");
}
Ok(())
}
fn file_unchanged(path: &Path, rendered: &str) -> bool {
fs::read_to_string(path)
.map(|on_disk| on_disk == rendered)
.unwrap_or(false)
}
#[aristo::intent(
"Per-annotation markdown structure is locked by the I1 `samples.md` \
mockup: header line (`**Aristo verified intent — \\`<id>\\`**` for \
intents, `**Aristo assumption — \\`<id>\\`**` for assumes), blank \
line, body text verbatim, blank line, `<sub>` metadata line, \
blank line, `---`. The metadata line composes verify-level + \
server-bound marker + parent link with ` · ` separators. A \
regression that drops the trailing `---` would break readers \
that include this MD with `include_str!` between other doc \
blocks — the separator is what isolates this annotation from \
surrounding rustdoc.",
verify = "neural",
id = "doc_per_annotation_md_shape_locked_by_samples_mockup"
)]
fn render_annotation_md(id: &AnnotationId, entry: &IndexEntry, include_status: bool) -> String {
let header = match entry {
IndexEntry::Intent(_) => format!("**Aristo verified intent — `{id}`**"),
IndexEntry::Assume(_) => format!("**Aristo assumption — `{id}`**"),
};
let text = match entry {
IndexEntry::Intent(e) => e.text.as_str(),
IndexEntry::Assume(e) => e.text.as_str(),
};
let meta = match entry {
IndexEntry::Intent(e) => intent_meta_line(id, e),
IndexEntry::Assume(e) => assume_meta_line(e),
};
let status_block = if include_status {
match entry {
IndexEntry::Intent(e) => Some(status_block_for_intent(e)),
IndexEntry::Assume(_) => None,
}
} else {
None
};
match status_block {
Some(sb) => format!("{header}\n\n{text}\n\n{meta}\n\n{sb}\n\n---\n"),
None => format!("{header}\n\n{text}\n\n{meta}\n\n---\n"),
}
}
#[aristo::intent(
"The `--include-status` block is a blockquote that records the \
status at MD-generation time. The icon + label are stable; \
dropping the `(this state is current as of …)` disclaimer would \
mislead readers into thinking the embedded status is live, which \
it isn't — it goes stale the moment source code changes. The \
disclaimer is what keeps the doc artifact honest.",
verify = "neural",
id = "doc_include_status_block_records_state_with_staleness_disclaimer"
)]
fn status_block_for_intent(e: &IntentEntry) -> String {
let icon_state = match e.status {
Status::Verified => "✓ verified",
Status::Tested => "✓ tested",
Status::Neural => "✓ neural",
Status::Stale => "⚠ stale",
Status::Orphan => "⚠ orphan",
Status::Forged => "⚠ forged",
Status::Counterexample => "✗ counterexample",
Status::PendingDeepen => "? pending-deepen",
Status::Unknown => "? unknown",
Status::Inconclusive => "? inconclusive",
};
let commit_hint = match &e.binding {
BindingState::Certified {
last_verified_at_commit,
..
} => {
let hex = last_verified_at_commit.as_str();
let short: String = hex.chars().take(10).collect();
format!(" at commit `{short}…`")
}
_ => String::new(),
};
format!(
"> **Verification state:** {icon_state}{commit_hint}\n\
> *(this state is current as of the last `aristo doc --include-status` run; re-run to refresh)*"
)
}
fn intent_meta_line(id: &AnnotationId, e: &IntentEntry) -> String {
let mut pieces: Vec<String> = Vec::new();
pieces.push(format!("Verify level: **{}**", verify_label(e.verify)));
if matches!(id.namespace(), IdNamespace::Aristos) {
pieces.push("Server-bound (`aristos:` namespace)".to_string());
}
if let Some(parent_text) = parent_link_text(e.parent.as_ref()) {
pieces.push(parent_text);
}
format!("<sub>{}</sub>", pieces.join(" · "))
}
fn assume_meta_line(e: &AssumeEntry) -> String {
let mut pieces: Vec<String> = vec!["Background fact (no verification target)".to_string()];
if let Some(parent_text) = parent_link_text(e.parent.as_ref()) {
pieces.push(parent_text);
}
format!("<sub>{}.</sub>", pieces.join(" · "))
}
fn parent_link_text(parent: Option<&ParentLink>) -> Option<String> {
let parent = parent?;
let parts: Vec<String> = parent
.iter()
.map(|p| format!("[parent: {p}](./{}.md)", id_safe(p)))
.collect();
Some(format!("See {}", parts.join(", ")))
}
fn verify_label(level: VerifyLevel) -> &'static str {
match level {
VerifyLevel::Bool(false) => "false",
VerifyLevel::Bool(true) => "true",
VerifyLevel::Method(VerifyMethod::Neural) => "neural",
VerifyLevel::Method(VerifyMethod::Test) => "test",
VerifyLevel::Method(VerifyMethod::Full) => "full",
}
}
#[aristo::intent(
"`aristo doc --summary` writes the crate-root `_summary.md` ONLY — \
it does not also run the per-annotation pass. Combining both is \
`aristo doc --include-graph` (slice 29). A regression that made \
`--summary` imply per-annotation writes would surprise users who \
opted into the cheap summary-only flow for CI gates.",
verify = "neural",
id = "doc_summary_writes_summary_only"
)]
fn run_summary(ws: &Workspace, index: &IndexFile) -> CliResult<()> {
let summary_path = ws.root.join(".aristo").join("doc").join("_summary.md");
let counts = Counts::from(index);
let md = render_summary_md(&counts);
if let Some(parent) = summary_path.parent() {
fs::create_dir_all(parent).map_err(CliError::Io)?;
}
fs::write(&summary_path, &md).map_err(CliError::Io)?;
println!();
println!("→ Reading .aristo/index.toml … ok");
println!("→ Writing .aristo/doc/_summary.md");
println!(
" • {} annotations ({} intent · {} assume)",
counts.total, counts.intent, counts.assume,
);
println!(
" • {} server-bound (aristos: namespace)",
counts.server_bound,
);
println!(
" • Verify levels: false={}, neural={}, test={}, full={}",
counts.verify_false, counts.verify_neural, counts.verify_test, counts.verify_full,
);
println!();
println!("ok: crate-root summary written.");
println!();
println!("To render in `cargo doc`, add to your lib.rs / main.rs:");
println!(" //! ...your existing crate doc...");
println!(" #![doc = include_str!(\"../.aristo/doc/_summary.md\")]");
Ok(())
}
#[derive(Debug, Default)]
struct Counts {
total: usize,
intent: usize,
assume: usize,
server_bound: usize,
verify_false: usize,
verify_neural: usize,
verify_test: usize,
verify_full: usize,
}
impl Counts {
fn from(index: &IndexFile) -> Self {
let mut c = Counts::default();
for (id, entry) in &index.entries {
c.total += 1;
if matches!(id.namespace(), IdNamespace::Aristos) {
c.server_bound += 1;
}
match entry {
IndexEntry::Intent(e) => {
c.intent += 1;
c.tally_verify(e);
}
IndexEntry::Assume(_) => c.assume += 1,
}
}
c
}
fn tally_verify(&mut self, e: &IntentEntry) {
match e.verify {
VerifyLevel::Bool(false) => self.verify_false += 1,
VerifyLevel::Bool(true) => self.verify_test += 1,
VerifyLevel::Method(VerifyMethod::Neural) => self.verify_neural += 1,
VerifyLevel::Method(VerifyMethod::Test) => self.verify_test += 1,
VerifyLevel::Method(VerifyMethod::Full) => self.verify_full += 1,
}
}
}
fn render_summary_md(c: &Counts) -> String {
format!(
"## Aristo verified annotations\n\
\n\
This crate carries **{total} Aristo annotations** ({intent} intent · {assume} assume).\n\
\n\
| Verify level | Count |\n\
|---|---|\n\
| `false` (documentation only) | {vfalse} |\n\
| `\"neural\"` | {vneural} |\n\
| `\"test\"` | {vtest} |\n\
| `\"full\"` | {vfull} |\n\
\n\
**{bound} annotations are server-bound** (`aristos:` namespace) and verified by the\n\
Aristo proof system. See the [annotation graph](./_graph.svg) for the full\n\
property structure.\n\
\n\
---\n",
total = c.total,
intent = c.intent,
assume = c.assume,
vfalse = c.verify_false,
vneural = c.verify_neural,
vtest = c.verify_test,
vfull = c.verify_full,
bound = c.server_bound,
)
}
fn id_safe(id: &AnnotationId) -> String {
id.as_str().replace(':', "__")
}
#[cfg(test)]
mod tests {
use super::*;
use aristo_core::index::{
AnnotationId, ArtaId, AssumeEntry, BindingState, CommitHash, CoveredRegion, IndexEntry,
IndexFile, IntentEntry, Meta, Sha256, Status, VerifiedOutcome,
};
use std::collections::BTreeMap;
fn sha(c: char) -> Sha256 {
Sha256::parse(&format!("sha256:{}", c.to_string().repeat(64))).unwrap()
}
fn intent(verify: VerifyLevel, server_bound: bool) -> IndexEntry {
IndexEntry::Intent(IntentEntry {
text: "x".into(),
verify,
status: Status::Unknown,
text_hash: sha('a'),
body_hash: sha('b'),
file: "src/lib.rs".into(),
site: "fn x (line 1)".into(),
covered_region: CoveredRegion::Function,
binding: if server_bound {
BindingState::Certified {
linked: ArtaId::parse("arta_op4q3z9NbV").unwrap(),
verified_outcome: VerifiedOutcome::parse(&format!("v1:{}", "A".repeat(86)))
.unwrap(),
last_verified_at_commit: CommitHash::parse(&"a".repeat(40)).unwrap(),
}
} else {
BindingState::Local
},
parent: None,
last_critiqued_at_text_hash: None,
last_critique_finding_count: None,
})
}
fn assume() -> IndexEntry {
IndexEntry::Assume(AssumeEntry {
text: "y".into(),
status: Status::Unknown,
text_hash: sha('a'),
body_hash: sha('b'),
file: "src/lib.rs".into(),
site: "fn y (line 2)".into(),
covered_region: CoveredRegion::Function,
linked: None,
parent: None,
})
}
fn make_index(entries: Vec<(&str, IndexEntry)>) -> IndexFile {
let mut map = BTreeMap::new();
for (id, entry) in entries {
map.insert(AnnotationId::parse(id).unwrap(), entry);
}
IndexFile {
meta: Meta {
schema_version: 1,
generated_by: None,
generated_at: None,
source_root: None,
},
entries: map,
}
}
#[test]
fn counts_tally_intent_assume_and_server_bound() {
let index = make_index(vec![
("local_intent", intent(VerifyLevel::Bool(false), false)),
(
"aristos:bound_intent",
intent(VerifyLevel::Method(VerifyMethod::Full), true),
),
("an_assume", assume()),
]);
let c = Counts::from(&index);
assert_eq!(c.total, 3);
assert_eq!(c.intent, 2);
assert_eq!(c.assume, 1);
assert_eq!(c.server_bound, 1);
assert_eq!(c.verify_false, 1);
assert_eq!(c.verify_full, 1);
}
#[test]
fn counts_buckets_each_verify_level() {
let index = make_index(vec![
("a", intent(VerifyLevel::Bool(false), false)),
(
"b",
intent(VerifyLevel::Method(VerifyMethod::Neural), false),
),
("c", intent(VerifyLevel::Method(VerifyMethod::Test), false)),
("d", intent(VerifyLevel::Method(VerifyMethod::Full), false)),
("e", intent(VerifyLevel::Bool(true), false)),
]);
let c = Counts::from(&index);
assert_eq!(c.verify_false, 1);
assert_eq!(c.verify_neural, 1);
assert_eq!(c.verify_test, 2);
assert_eq!(c.verify_full, 1);
}
#[test]
fn render_summary_md_includes_counts_and_table_header() {
let c = Counts {
total: 47,
intent: 33,
assume: 14,
server_bound: 20,
verify_false: 9,
verify_neural: 4,
verify_test: 12,
verify_full: 22,
};
let md = render_summary_md(&c);
assert!(md.contains("**47 Aristo annotations**"));
assert!(md.contains("(33 intent · 14 assume)"));
assert!(md.contains("| Verify level | Count |"));
assert!(md.contains("| `false` (documentation only) | 9 |"));
assert!(md.contains("**20 annotations are server-bound**"));
}
#[test]
fn id_safe_replaces_colon_with_double_underscore() {
let id = AnnotationId::parse("aristos:balance_no_duplicate_cells").unwrap();
assert_eq!(id_safe(&id), "aristos__balance_no_duplicate_cells");
}
#[test]
fn id_safe_leaves_local_id_unchanged() {
let id = AnnotationId::parse("cells_extracted_without_aliasing").unwrap();
assert_eq!(id_safe(&id), "cells_extracted_without_aliasing");
}
#[test]
fn render_intent_uses_aristo_verified_intent_header() {
let id = AnnotationId::parse("aristos:balance_no_duplicate_cells").unwrap();
let entry = intent(VerifyLevel::Method(VerifyMethod::Full), true);
let md = render_annotation_md(&id, &entry, false);
assert!(
md.starts_with("**Aristo verified intent — `aristos:balance_no_duplicate_cells`**\n"),
"got:\n{md}"
);
}
#[test]
fn render_assume_uses_aristo_assumption_header() {
let id = AnnotationId::parse("storage_write_atomicity").unwrap();
let entry = assume();
let md = render_annotation_md(&id, &entry, false);
assert!(
md.starts_with("**Aristo assumption — `storage_write_atomicity`**\n"),
"got:\n{md}"
);
}
#[test]
fn render_intent_metadata_marks_server_bound_for_aristos_namespace() {
let id = AnnotationId::parse("aristos:bound").unwrap();
let entry = intent(VerifyLevel::Method(VerifyMethod::Full), true);
let md = render_annotation_md(&id, &entry, false);
assert!(
md.contains("Verify level: **full**"),
"expected verify-level marker; got:\n{md}"
);
assert!(
md.contains("Server-bound (`aristos:` namespace)"),
"expected server-bound marker; got:\n{md}"
);
}
#[test]
fn render_intent_metadata_omits_server_bound_for_local() {
let id = AnnotationId::parse("local_intent").unwrap();
let entry = intent(VerifyLevel::Bool(false), false);
let md = render_annotation_md(&id, &entry, false);
assert!(
md.contains("Verify level: **false**"),
"expected verify-level marker; got:\n{md}"
);
assert!(
!md.contains("Server-bound"),
"did not expect server-bound marker on local entry; got:\n{md}"
);
}
#[test]
fn render_metadata_includes_parent_link() {
use aristo_core::index::ParentLink;
let id = AnnotationId::parse("child").unwrap();
let mut entry = match intent(VerifyLevel::Method(VerifyMethod::Test), false) {
IndexEntry::Intent(e) => e,
_ => unreachable!(),
};
entry.parent = Some(ParentLink::Single(
AnnotationId::parse("balance_no_cells_lost").unwrap(),
));
let md = render_annotation_md(&id, &IndexEntry::Intent(entry), false);
assert!(
md.contains("See [parent: balance_no_cells_lost](./balance_no_cells_lost.md)"),
"expected parent link; got:\n{md}"
);
}
#[test]
fn render_ends_with_separator_rule() {
let id = AnnotationId::parse("any").unwrap();
let entry = intent(VerifyLevel::Bool(false), false);
let md = render_annotation_md(&id, &entry, false);
assert!(
md.trim_end().ends_with("\n---"),
"expected trailing `---` separator; got:\n{md}"
);
}
#[test]
fn render_intent_with_include_status_appends_blockquote() {
let id = AnnotationId::parse("aristos:bound").unwrap();
let entry = intent(VerifyLevel::Method(VerifyMethod::Full), true);
let entry = match entry {
IndexEntry::Intent(mut e) => {
e.status = Status::Verified;
IndexEntry::Intent(e)
}
other => other,
};
let md = render_annotation_md(&id, &entry, true);
assert!(
md.contains("> **Verification state:** ✓ verified"),
"expected verified blockquote; got:\n{md}"
);
assert!(
md.contains("state is current as of the last"),
"expected staleness disclaimer; got:\n{md}"
);
}
#[test]
fn render_intent_with_include_status_includes_commit_hint_for_certified() {
let id = AnnotationId::parse("aristos:bound").unwrap();
let entry = intent(VerifyLevel::Method(VerifyMethod::Full), true);
let md = render_annotation_md(&id, &entry, true);
assert!(
md.contains("at commit `aaaaaaaaaa…`"),
"expected 10-char commit hint; got:\n{md}"
);
}
#[test]
fn render_intent_with_include_status_omits_commit_hint_for_local() {
let id = AnnotationId::parse("local").unwrap();
let entry = intent(VerifyLevel::Bool(false), false);
let md = render_annotation_md(&id, &entry, true);
assert!(
!md.contains("at commit"),
"did not expect commit hint on local entry; got:\n{md}"
);
}
#[test]
fn render_assume_with_include_status_omits_blockquote() {
let id = AnnotationId::parse("storage_write_atomicity").unwrap();
let entry = assume();
let md = render_annotation_md(&id, &entry, true);
assert!(
!md.contains("Verification state"),
"did not expect blockquote on assume; got:\n{md}"
);
}
#[test]
fn status_block_uses_warn_icon_for_stale() {
let mut e = match intent(VerifyLevel::Method(VerifyMethod::Full), true) {
IndexEntry::Intent(e) => e,
_ => unreachable!(),
};
e.status = Status::Stale;
let block = status_block_for_intent(&e);
assert!(block.contains("⚠ stale"), "got: {block}");
}
#[test]
fn status_block_uses_cross_icon_for_counterexample() {
let mut e = match intent(VerifyLevel::Method(VerifyMethod::Full), true) {
IndexEntry::Intent(e) => e,
_ => unreachable!(),
};
e.status = Status::Counterexample;
let block = status_block_for_intent(&e);
assert!(block.contains("✗ counterexample"), "got: {block}");
}
#[test]
fn file_unchanged_returns_true_for_byte_equal_disk_content() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), "hello\n").unwrap();
assert!(file_unchanged(tmp.path(), "hello\n"));
}
#[test]
fn file_unchanged_returns_false_for_drift() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), "before\n").unwrap();
assert!(!file_unchanged(tmp.path(), "after\n"));
}
#[test]
fn file_unchanged_returns_false_for_missing_file() {
let tmp = tempfile::TempDir::new().unwrap();
let missing = tmp.path().join("nope.md");
assert!(!file_unchanged(&missing, "anything"));
}
}