1use std::collections::{BTreeMap, BTreeSet};
36use std::path::Path;
37
38use serde::Serialize;
39
40use crate::Engine;
41use crate::anchor::{AnchorGrain, AnchorProvenanceClass, AnchorState};
42use crate::binding::{Binding, CoverageSemantics, MediumCapabilities, medium_capabilities};
43use crate::chunking::estimate_tokens;
44
45use super::advance::read_advance_store;
46use super::cursor::{enumerate_source_artifacts_reported, source_moved};
47use super::findings::{FindingClass, FindingKey, read_findings_store};
48use super::resolve::{ChangeStrategy, ResolvedIngest, ResolvedSource, resolve_change_strategy};
49
50pub const DEFAULT_REPORT_BUDGET: usize = 8_000;
54
55pub const ALLOWED_REPORT_INCLUDE_KEYS: &[&str] =
61 &["uncovered_artifacts", "tree_fanout", "superseded_findings"];
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
74#[serde(tag = "kind", rename_all = "kebab-case")]
75pub enum DenominatorBasis {
76 Enumerated {
79 count: usize,
81 },
82 NonEnumerable {
86 reason: String,
88 },
89 Partial {
95 count: usize,
97 reason: String,
99 },
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
106pub struct TreeFanout {
107 pub entity: String,
109 pub artifact: String,
111 pub fanout: usize,
113}
114
115pub const COVERAGE_UNIT: &str = "describing entities per artifact — an artifact counts once \
117 when at least one entity anchors it, however many anchor rows it carries; anchor rows \
118 are counted on the resolution axis, never here";
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
124pub struct GrainCoverage {
125 pub denominator: DenominatorBasis,
127 pub covered_artifacts: usize,
132 pub describing_entities: usize,
135 pub unit: &'static str,
138 pub direct_covered: usize,
140 pub tree_only_covered: usize,
143 pub uncovered: Vec<String>,
145 pub tree_anchors: Vec<TreeFanout>,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
153pub struct AnchorComposition {
154 pub by_class: BTreeMap<String, usize>,
158 pub by_grain: BTreeMap<String, usize>,
160 pub authored: usize,
163 pub observed: usize,
165 pub resolves: usize,
167 pub drifted: usize,
169 pub recheck: usize,
171 pub orphaned: usize,
173 pub unobserved: usize,
176 pub counted_rows: usize,
182 pub distinct_artifacts: usize,
187 pub excluded_other_binding: usize,
190 pub excluded_out_of_scope: usize,
193 pub excluded_artifacts: Vec<String>,
197 pub counted_without_provenance: usize,
201 pub dangling: usize,
206 pub dangling_rows: Vec<String>,
210 pub unreconciled: Option<String>,
214 pub span_unvalidated: usize,
219 pub hash_from_backfill: usize,
224 pub aging: Vec<AgingAnchor>,
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
233pub struct AgingAnchor {
234 pub entity: String,
235 pub artifact: String,
236 pub observed_at: String,
237 pub unobserved_for_days: u64,
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
243pub struct FacetCapability {
244 pub facet: String,
246 pub medium_type: String,
248 pub enumerable: bool,
250 pub change_signal: bool,
252 pub base_version_retrievable: bool,
254 pub anchor_namespace: String,
256 pub signal: String,
259}
260
261impl FacetCapability {
262 fn from_caps(
263 facet: String,
264 medium_type: String,
265 caps: MediumCapabilities,
266 strategy: ChangeStrategy,
267 ) -> Self {
268 FacetCapability {
269 facet,
270 medium_type,
271 enumerable: caps.enumerable,
272 change_signal: caps.change_signal,
273 base_version_retrievable: caps.base_version_retrievable
280 && strategy_retrieves_base(strategy),
281 anchor_namespace: caps.anchor_namespace.to_string(),
282 signal: signal_wire(strategy).to_string(),
283 }
284 }
285}
286
287#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
289pub struct FacetFreshness {
290 pub facet: String,
292 pub signal: String,
294 pub synced: Option<String>,
296 pub verified: Option<String>,
298 pub change_detectable: bool,
303}
304
305#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
307pub struct FidelityReport {
308 pub binding: String,
310 pub destination_mem: String,
312 pub adopt: bool,
319 pub coverage_semantics: CoverageSemantics,
323 pub coverage_semantics_declared: bool,
328 pub legacy_dialect_patterns: Vec<String>,
334 pub capabilities: Vec<FacetCapability>,
336 pub freshness: Vec<FacetFreshness>,
338 pub source_moved_past_synced: Option<bool>,
342 pub coverage: GrainCoverage,
344 pub anchors: AnchorComposition,
346 pub findings_by_class: BTreeMap<String, usize>,
348 pub backlog: usize,
350 pub superseded: Vec<String>,
353 pub disposed_excluded: usize,
356 pub disposed_excluded_rationales: Vec<(String, String)>,
362 pub degradations: Vec<String>,
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
382#[serde(rename_all = "lowercase")]
383pub enum RollupVerdict {
384 Clean,
386 Drifted,
388 Inconclusive,
391}
392
393impl RollupVerdict {
394 pub fn wire(&self) -> &'static str {
396 match self {
397 RollupVerdict::Clean => "clean",
398 RollupVerdict::Drifted => "drifted",
399 RollupVerdict::Inconclusive => "inconclusive",
400 }
401 }
402}
403
404#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
407pub struct Rollup {
408 pub verdict: RollupVerdict,
410 pub findings_total: usize,
412 pub because: String,
415 pub blind_spots: Vec<String>,
420 pub actions: Vec<String>,
423}
424
425const CLASS_SEVERITY: [&str; 5] = [
430 "wrong",
431 "drifted",
432 "unresolvable-anchor",
433 "uncovered",
434 "queued-for-adjudication",
435];
436
437fn class_action(class: &str, n: usize, binding: &str) -> String {
439 match class {
440 "wrong" => format!(
441 "{n} entity/entities contradict their source — read them against the source and \
442 correct the entity (`memstead projection brief {binding}` lists them)"
443 ),
444 "drifted" => format!(
445 "{n} anchored artifact(s) moved since the entity was written — re-read the source \
446 and update the entity, then re-verify to advance the baseline"
447 ),
448 "unresolvable-anchor" => format!(
449 "{n} anchor(s) no longer resolve to anything — repoint them at the artifact's new \
450 location or unset them (`memstead_update` `anchors_unset`)"
451 ),
452 "uncovered" => format!(
453 "{n} in-scope source artifact(s) carry no anchor — cover them via \
454 `memstead projection brief {binding} --sync`, or record a disposition for the \
455 ones deliberately excluded"
456 ),
457 "queued-for-adjudication" => format!(
458 "{n} finding(s) are queued and not yet adjudicated — run \
459 `memstead projection verify {binding} --full` to work the backlog down"
460 ),
461 other => format!("{n} `{other}` finding(s) recorded"),
462 }
463}
464
465impl FidelityReport {
466 pub fn rollup(&self) -> Rollup {
473 let findings_total: usize = self.findings_by_class.values().sum();
474
475 let mut blind_spots: Vec<String> = Vec::new();
476 match &self.coverage.denominator {
477 DenominatorBasis::NonEnumerable { reason } => blind_spots.push(format!(
478 "the source scope is not enumerable ({reason}) — coverage is reported over \
479 anchors only, so an uncovered artifact cannot be detected"
480 )),
481 DenominatorBasis::Enumerated { count: 0 } => blind_spots.push(
482 "the enumerated source scope is empty (0 artifacts) — every coverage figure \
483 below is vacuous, not clean"
484 .to_string(),
485 ),
486 DenominatorBasis::Partial { count, reason } => blind_spots.push(format!(
487 "the source enumeration is INCOMPLETE ({reason}) — {count} artifact(s) \
488 survived, but their share of the population is unknown, so no coverage \
489 percentage is reported below"
490 )),
491 DenominatorBasis::Enumerated { .. } => {}
492 }
493 if !self.legacy_dialect_patterns.is_empty() {
494 blind_spots.push(format!(
495 "scope pattern(s) are still written against the workspace root rather than the \
496 source pointer and select nothing under the pointer join, so whatever they \
497 were meant to cover is absent from the denominator: {}. Rewrite them relative \
498 to the source's pointer",
499 self.legacy_dialect_patterns.join(", ")
500 ));
501 }
502 if self.anchors.observed == 0 {
503 blind_spots.push(
504 "no anchor carried a resolution state this pass — nothing was adjudicated"
505 .to_string(),
506 );
507 }
508 if self.anchors.unobserved > 0 {
520 blind_spots.push(format!(
521 "{} counted anchor(s) could not be observed at all this pass, so their state is unknown rather than clean",
522 self.anchors.unobserved
523 ));
524 }
525 if self.anchors.span_unvalidated > 0 {
526 blind_spots.push(format!(
527 "{} counted span anchor(s) were never checked against their artifact, so the span they name is unverified even where the hash resolves",
528 self.anchors.span_unvalidated
529 ));
530 }
531 if let Some(why) = &self.anchors.unreconciled {
532 blind_spots.push(format!(
533 "the entity end of these anchors was not reconciled ({why}), so a row naming an entity the mem no longer holds would not have been detected"
534 ));
535 }
536 let change_blind: std::collections::BTreeSet<&str> = self
546 .freshness
547 .iter()
548 .filter(|f| !f.change_detectable)
549 .map(|f| f.facet.as_str())
550 .collect();
551 for cap in &self.capabilities {
552 if !cap.change_signal {
553 blind_spots.push(format!(
554 "facet `{}` ({}) provides no change signal — drift on it cannot be \
555 observed at all",
556 cap.facet, cap.medium_type
557 ));
558 } else if change_blind.contains(cap.facet.as_str()) {
559 blind_spots.push(format!(
560 "facet `{}` ({}) declares change-detection `{}` but this pass could \
561 not read that signal — either the binding asked for none, or the \
562 checkout cannot deliver it (a `git` source with no `.git`: an \
563 archive, a container COPY, a vendored drop). Drift on it cannot \
564 be observed",
565 cap.facet, cap.medium_type, cap.signal
566 ));
567 }
568 if !cap.enumerable {
578 blind_spots.push(format!(
579 "facet `{}` ({}) is not enumerable — an uncovered artifact under it \
580 cannot be detected, only an anchored one",
581 cap.facet, cap.medium_type
582 ));
583 }
584 }
585
586 let mut actions: Vec<String> = Vec::new();
587 for class in CLASS_SEVERITY {
588 if let Some(&n) = self.findings_by_class.get(class)
589 && n > 0
590 {
591 actions.push(class_action(class, n, &self.binding));
592 }
593 }
594 for (class, &n) in &self.findings_by_class {
597 if n > 0 && !CLASS_SEVERITY.contains(&class.as_str()) {
598 actions.push(class_action(class, n, &self.binding));
599 }
600 }
601
602 let only_uncovered = findings_total > 0
608 && self
609 .findings_by_class
610 .iter()
611 .all(|(class, &n)| n == 0 || class == "uncovered");
612
613 let (verdict, because) = if self.adopt && only_uncovered {
614 (
615 RollupVerdict::Inconclusive,
616 format!(
617 "this mem predates its binding — the {findings_total} uncovered artifact(s) \
618 are the backfill worklist, not drift"
619 ),
620 )
621 } else if findings_total > 0 {
622 let tally = self
623 .findings_by_class
624 .iter()
625 .filter(|(_, n)| **n > 0)
626 .map(|(class, n)| format!("{class}: {n}"))
627 .collect::<Vec<_>>()
628 .join(", ");
629 (
630 RollupVerdict::Drifted,
631 format!("{findings_total} finding(s) recorded over the current key ({tally})"),
632 )
633 } else if !blind_spots.is_empty() {
634 (
635 RollupVerdict::Inconclusive,
636 format!(
637 "no findings recorded, but the pass could not speak to {} axis/axes — \
638 this is not a clean bill of health",
639 blind_spots.len()
640 ),
641 )
642 } else {
643 (
644 RollupVerdict::Clean,
645 "the pass was substantive on every axis and recorded no findings".to_string(),
646 )
647 };
648
649 Rollup {
650 verdict,
651 findings_total,
652 because,
653 blind_spots,
654 actions,
655 }
656 }
657}
658
659#[derive(Debug, Clone, PartialEq, Eq)]
666pub struct RenderedFidelityReport {
667 pub markdown: String,
669 pub mode: String,
672 pub hints: Vec<(String, usize)>,
675 pub budget_used: usize,
677}
678
679fn ratio(num: usize, den: usize) -> String {
685 if den == 0 {
686 format!("{num}/{den} (n/a)")
687 } else {
688 let pct = (num as f64) * 100.0 / (den as f64);
689 format!("{num}/{den} ({pct:.1}%)")
690 }
691}
692
693fn render_hard_required(report: &FidelityReport) -> String {
697 let mut md = String::new();
698 md.push_str(&format!("# Fidelity report — `{}`\n\n", report.binding));
699
700 let rollup = report.rollup();
705 md.push_str(&format!(
706 "**Verdict: {}** — {}.\n\n",
707 rollup.verdict.wire().to_uppercase(),
708 rollup.because
709 ));
710 if !rollup.actions.is_empty() {
711 md.push_str("**Do next:**\n\n");
712 for action in &rollup.actions {
713 md.push_str(&format!("1. {action}\n"));
714 }
715 md.push('\n');
716 }
717 if !rollup.blind_spots.is_empty() {
718 md.push_str("**This pass could not see:**\n\n");
719 for spot in &rollup.blind_spots {
720 md.push_str(&format!("- {spot}\n"));
721 }
722 md.push('\n');
723 }
724
725 md.push_str(&format!(
726 "- **Destination mem:** `{}`\n- **Coverage semantics:** {}{}\n\n",
727 report.destination_mem,
728 match report.coverage_semantics {
729 CoverageSemantics::Exhaustive => "exhaustive",
730 CoverageSemantics::Curated => "curated",
731 },
732 if report.coverage_semantics_declared {
733 ""
734 } else {
735 " (resolved from the sources' media — not declared)"
736 }
737 ));
738
739 if report.adopt {
746 md.push_str("## Adopting — first verify\n\n");
747 md.push_str(
748 "This mem predates its binding: it carries no anchors and has no prior sync \
749 baseline, so **0% anchored is expected — this is onboarding, not a failure.** \
750 Do not read the coverage numbers below as drift or a red verdict; the uncovered \
751 artifacts are the backfill worklist, not defects.\n\n",
752 );
753 md.push_str(&format!(
754 "**Backfill path:** run `memstead projection brief {} --sync` to work through the in-scope \
755 source artifacts that carry no entity yet, covering the clearly-new concepts among \
756 them through the normal mutation surface. Backfilling is incremental — a partial \
757 pass is fine, and the next sync continues where you left off.\n\n",
758 report.binding
759 ));
760 }
761
762 md.push_str("## Denominator provenance\n\n");
764 match &report.coverage.denominator {
765 DenominatorBasis::Enumerated { count } => md.push_str(&format!(
766 "Coverage is reported relative to the per-medium enumeration `S(D)` = **{count}** \
767 source artifact(s) in scope (after `deny_paths`).\n\n"
768 )),
769 DenominatorBasis::NonEnumerable { reason } => md.push_str(&format!(
770 "No `S(D)` denominator: {reason}. Coverage is reported over anchors only; the \
771 per-medium enumeration is unavailable.\n\n"
772 )),
773 DenominatorBasis::Partial { count, reason } => md.push_str(&format!(
774 "`S(D)` is **partial**: {reason}. **{count}** source artifact(s) were \
775 enumerated by the patterns that did resolve, but that set is not the \
776 population, so the coverage figures below are counts and carry no \
777 percentage.\n\n"
778 )),
779 }
780
781 md.push_str("## Capability matrix\n\n");
783 if report.capabilities.is_empty() {
784 md.push_str("_(no primary sources resolved)_\n\n");
785 } else {
786 for c in &report.capabilities {
787 md.push_str(&format!("### `{}` ({})\n\n", c.facet, c.medium_type));
788 md.push_str(&format!(
789 "- enumerable: {} | change_signal: {} | base_version_retrievable: {}\n",
790 c.enumerable, c.change_signal, c.base_version_retrievable
791 ));
792 md.push_str(&format!(
793 "- anchor_namespace: `{}` | resolved signal: `{}`\n\n",
794 c.anchor_namespace, c.signal
795 ));
796 }
797 }
798
799 md.push_str("## Freshness\n\n");
801 if report.freshness.is_empty() {
802 md.push_str("_(no source facets)_\n\n");
803 } else {
804 for f in &report.freshness {
805 md.push_str(&format!("### `{}`\n\n", f.facet));
806 md.push_str(&format!("- signal: `{}`\n", f.signal));
807 if !f.change_detectable {
808 md.push_str(
812 "- **freshness unknowable** — this medium is not change-detectable \
813 (no change signal); `#synced` / `#verified` cannot be adjudicated as fresh\n",
814 );
815 } else {
816 match &f.synced {
817 Some(t) => md.push_str(&format!("- `#synced`: `{t}`\n")),
818 None => md.push_str("- `#synced`: never synced\n"),
819 }
820 match &f.verified {
821 Some(t) => md.push_str(&format!("- `#verified`: `{t}`\n")),
822 None => md.push_str("- `#verified`: never verified\n"),
823 }
824 }
825 md.push('\n');
826 }
827 match report.source_moved_past_synced {
829 Some(true) => md.push_str(
830 "**Source moved past its `#synced` baseline** — the graph is stale for the \
831 moved facet(s); a sync pass is due.\n\n",
832 ),
833 Some(false) => {
834 md.push_str("Every change-detectable source is at its `#synced` baseline.\n\n")
835 }
836 None => {}
837 }
838 }
839
840 md.push_str("## Coverage (grain-classed)\n\n");
842 let den = match &report.coverage.denominator {
846 DenominatorBasis::Enumerated { count } => *count,
847 DenominatorBasis::NonEnumerable { .. } | DenominatorBasis::Partial { .. } => 0,
848 };
849 md.push_str(&format!(
850 "- direct-covered (file / span anchors): {}\n",
851 ratio(report.coverage.direct_covered, den)
852 ));
853 let tree_files: usize = report.coverage.tree_anchors.iter().map(|t| t.fanout).sum();
856 md.push_str(&format!(
857 "- tree-anchor fan-out (separate axis): {} tree anchor(s) fanning out over {} file(s); \
858 {} file(s) covered ONLY via a tree anchor\n",
859 report.coverage.tree_anchors.len(),
860 tree_files,
861 report.coverage.tree_only_covered
862 ));
863 md.push_str(&format!(
864 "- uncovered (no anchor): {}\n",
865 report.coverage.uncovered.len()
866 ));
867 md.push_str(&format!(
870 "- coverage unit: {} — {} describing entit{} over {} covered artifact(s)\n\n",
871 report.coverage.unit,
872 report.coverage.describing_entities,
873 if report.coverage.describing_entities == 1 {
874 "y"
875 } else {
876 "ies"
877 },
878 report.coverage.covered_artifacts
879 ));
880
881 match report.coverage_semantics {
886 CoverageSemantics::Exhaustive if report.adopt => {
887 let backlog = report
888 .coverage
889 .uncovered
890 .len()
891 .saturating_sub(report.disposed_excluded);
892 md.push_str(&format!(
893 "**Exhaustive coverage (onboarding):** {backlog} in-scope artifact(s) carry no \
894 entity yet ({} disposed excluded) — the expected first-sync backfill worklist \
895 for a mem that predates its binding, not defects.\n\n",
896 report.disposed_excluded
897 ));
898 }
899 CoverageSemantics::Exhaustive => {
900 let findings = report
901 .coverage
902 .uncovered
903 .len()
904 .saturating_sub(report.disposed_excluded);
905 md.push_str(&format!(
906 "**Exhaustive coverage:** {findings} unaccounted artifact(s) — not anchored, not \
907 declared-excluded, no persisted disposition ({} disposed excluded) — are \
908 **findings**.\n\n",
909 report.disposed_excluded
910 ));
911 }
912 CoverageSemantics::Curated => {
913 md.push_str(&format!(
914 "**Curated coverage:** {} unaccounted artifact(s) are **information**, not \
915 defects — a curated binding covers a deliberate slice.\n\n",
916 report.coverage.uncovered.len()
917 ));
918 }
919 }
920
921 if !report.disposed_excluded_rationales.is_empty() {
925 md.push_str("**Excluded on purpose (persisted dispositions):**\n");
926 for (artifact, rationale) in &report.disposed_excluded_rationales {
927 if rationale.is_empty() {
928 md.push_str(&format!("- `{artifact}`\n"));
929 } else {
930 md.push_str(&format!("- `{artifact}` — {rationale}\n"));
931 }
932 }
933 md.push('\n');
934 }
935
936 md.push_str("## Anchors\n\n");
938 md.push_str(&format!(
939 "- by class: {}\n",
940 render_counts(&report.anchors.by_class)
941 ));
942 md.push_str(&format!(
943 "- by grain: {}\n",
944 render_counts(&report.anchors.by_grain)
945 ));
946 md.push_str(&format!(
947 "- `authored` bucket (excluded from coverage/accuracy denominators): {}\n",
948 report.anchors.authored
949 ));
950 md.push_str(&format!(
957 "- resolution (non-`authored`, observed): resolves {}, drifted {}, recheck {}, \
958 orphaned {}; **anchor-resolution %:** {} over {} counted row(s) on {} distinct \
959 artifact(s), with {} unobserved this pass (state unavailable, never scored as \
960 resolved)\n",
961 report.anchors.resolves,
962 report.anchors.drifted,
963 report.anchors.recheck,
964 report.anchors.orphaned,
965 ratio(report.anchors.resolves, report.anchors.observed),
966 report.anchors.counted_rows,
967 report.anchors.distinct_artifacts,
968 report.anchors.unobserved
969 ));
970 md.push_str(&format!(
976 "- the figures above count anchor ROWS: {} row(s) over {} distinct artifact(s)\n",
977 report.anchors.counted_rows, report.anchors.distinct_artifacts
978 ));
979 if !report.anchors.aging.is_empty() {
983 md.push_str(&format!(
984 "- {} counted row(s) rest on a recorded observation rather than a live one \
985 (url anchors; the engine never fetches):\n",
986 report.anchors.aging.len()
987 ));
988 const AGING_CAP: usize = 10;
989 for a in report.anchors.aging.iter().take(AGING_CAP) {
990 md.push_str(&format!(
991 " - `{}` → `{}`: unobserved for {} days (observed {})\n",
992 a.entity, a.artifact, a.unobserved_for_days, a.observed_at
993 ));
994 }
995 if report.anchors.aging.len() > AGING_CAP {
996 md.push_str(&format!(
997 " - …and {} more\n",
998 report.anchors.aging.len() - AGING_CAP
999 ));
1000 }
1001 }
1002 if report.anchors.excluded_other_binding > 0 || report.anchors.excluded_out_of_scope > 0 {
1005 md.push_str(&format!(
1006 "- excluded from this binding's population: {} written by another binding, \
1007 {} outside this binding's declared scope (legal, reported here, never deleted)\n",
1008 report.anchors.excluded_other_binding, report.anchors.excluded_out_of_scope
1009 ));
1010 const NAMED_CAP: usize = 10;
1017 for a in report.anchors.excluded_artifacts.iter().take(NAMED_CAP) {
1018 md.push_str(&format!(" - {a}\n"));
1019 }
1020 if report.anchors.excluded_artifacts.len() > NAMED_CAP {
1021 md.push_str(&format!(
1022 " - …and {} more (counts above are complete)\n",
1023 report.anchors.excluded_artifacts.len() - NAMED_CAP
1024 ));
1025 }
1026 }
1027 match (&report.anchors.unreconciled, report.anchors.dangling) {
1032 (Some(why), _) => md.push_str(&format!(
1033 "- the entity end of these anchors was NOT reconciled this pass ({why}), so \
1034 dangling sidecar rows would not have been detected\n"
1035 )),
1036 (None, 0) => {}
1037 (None, n) => {
1038 md.push_str(&format!(
1039 "- {n} sidecar row(s) name an entity this mem no longer holds. Excluded from \
1040 every figure above, reported rather than repaired: the row is the trace of a \
1041 writer that went around the engine\n"
1042 ));
1043 const NAMED_CAP: usize = 10;
1044 for r in report.anchors.dangling_rows.iter().take(NAMED_CAP) {
1045 md.push_str(&format!(" - {r}\n"));
1046 }
1047 if report.anchors.dangling_rows.len() > NAMED_CAP {
1048 md.push_str(&format!(
1049 " - …and {} more (the count above is complete)\n",
1050 report.anchors.dangling_rows.len() - NAMED_CAP
1051 ));
1052 }
1053 }
1054 }
1055 if report.anchors.span_unvalidated > 0 {
1061 md.push_str(&format!(
1062 "- {} counted span row(s) were never checked against their artifact, so their \
1063 span is unverified even where the hash resolves\n",
1064 report.anchors.span_unvalidated
1065 ));
1066 }
1067 if report.anchors.hash_from_backfill > 0 {
1068 md.push_str(&format!(
1069 "- {} counted row(s) carry a baseline the engine inferred by backfill rather than \
1070 one an author pinned\n",
1071 report.anchors.hash_from_backfill
1072 ));
1073 }
1074 if report.anchors.counted_without_provenance > 0 {
1075 md.push_str(&format!(
1076 "- {} counted anchor(s) record no producing binding and are included by the \
1077 pre-provenance fallback, so this population rests partly on that fallback \
1078 rather than wholly on provenance\n",
1079 report.anchors.counted_without_provenance
1080 ));
1081 }
1082 md.push('\n');
1083
1084 md.push_str("## Findings\n\n");
1086 md.push_str(&format!(
1087 "- by class: {}\n",
1088 render_counts(&report.findings_by_class)
1089 ));
1090 md.push_str(&format!(
1091 "- **tier-3 adjudication backlog:** {}\n",
1092 report.backlog
1093 ));
1094 md.push_str(&format!(
1095 "- superseded (prior `hash(D)`, segregated): {}\n\n",
1096 report.superseded.len()
1097 ));
1098
1099 md.push_str("## Degradations\n\n");
1101 if report.degradations.is_empty() {
1102 md.push_str("_(none)_\n\n");
1103 } else {
1104 for d in &report.degradations {
1105 md.push_str(&format!("- {d}\n"));
1106 }
1107 md.push('\n');
1108 }
1109
1110 md
1111}
1112
1113fn render_counts(counts: &BTreeMap<String, usize>) -> String {
1115 if counts.is_empty() {
1116 return "(none)".to_string();
1117 }
1118 counts
1119 .iter()
1120 .map(|(k, v)| format!("{k}={v}"))
1121 .collect::<Vec<_>>()
1122 .join(", ")
1123}
1124
1125fn heavy_sections(report: &FidelityReport) -> Vec<(&'static str, String)> {
1129 let mut out: Vec<(&'static str, String)> = Vec::new();
1130
1131 let mut s = String::new();
1133 if !report.coverage.uncovered.is_empty() {
1134 s.push_str("## Uncovered artifacts\n\n");
1135 for a in &report.coverage.uncovered {
1136 s.push_str(&format!("- `{a}`\n"));
1137 }
1138 s.push('\n');
1139 }
1140 out.push(("uncovered_artifacts", s));
1141
1142 let mut s = String::new();
1144 if !report.coverage.tree_anchors.is_empty() {
1145 s.push_str("## Tree-anchor fan-out (detail)\n\n");
1146 for t in &report.coverage.tree_anchors {
1147 s.push_str(&format!(
1148 "- `{}` → `{}` fans out over {} file(s)\n",
1149 t.entity, t.artifact, t.fanout
1150 ));
1151 }
1152 s.push('\n');
1153 }
1154 out.push(("tree_fanout", s));
1155
1156 let mut s = String::new();
1158 if !report.superseded.is_empty() {
1159 s.push_str("## Superseded findings (detail)\n\n");
1160 for f in &report.superseded {
1161 s.push_str(&format!("- {f}\n"));
1162 }
1163 s.push('\n');
1164 }
1165 out.push(("superseded_findings", s));
1166
1167 out
1168}
1169
1170pub fn render_fidelity_report(
1181 report: &FidelityReport,
1182 budget: usize,
1183 include: &[String],
1184) -> RenderedFidelityReport {
1185 let hard = render_hard_required(report);
1186 let hard_cost = estimate_tokens(&hard);
1187 let overbudget = hard_cost > budget;
1188
1189 let include_set: std::collections::BTreeSet<&str> = include
1190 .iter()
1191 .map(String::as_str)
1192 .filter(|k| ALLOWED_REPORT_INCLUDE_KEYS.contains(k))
1193 .collect();
1194 let unknown_includes: Vec<&String> = include
1195 .iter()
1196 .filter(|k| !ALLOWED_REPORT_INCLUDE_KEYS.contains(&k.as_str()))
1197 .collect();
1198
1199 let sections = heavy_sections(report);
1200 let mut emitted: Vec<String> = Vec::new();
1201 let mut hints: Vec<(String, usize)> = Vec::new();
1202 let mut used = hard_cost;
1203 let mut remaining = budget.saturating_sub(hard_cost);
1204
1205 for (key, section_md) in §ions {
1206 if section_md.is_empty() {
1207 continue; }
1209 let cost = estimate_tokens(section_md);
1210 let forced = include_set.contains(key);
1211 if forced {
1212 emitted.push(section_md.clone());
1213 used += cost;
1214 remaining = remaining.saturating_sub(cost);
1215 } else if !overbudget && remaining >= cost {
1216 emitted.push(section_md.clone());
1217 used += cost;
1218 remaining -= cost;
1219 } else {
1220 hints.push(((*key).to_string(), cost));
1221 }
1222 }
1223
1224 let mode = if overbudget {
1225 "overbudget"
1226 } else if hints.is_empty() {
1227 "complete"
1228 } else {
1229 "reduced"
1230 };
1231
1232 let mut md = String::new();
1233 md.push_str("---\n");
1234 md.push_str(&format!("_report_mode: {mode}\n"));
1235 md.push_str(&format!("_budget_requested: {budget}\n"));
1236 md.push_str(&format!("_budget_used: {used}\n"));
1237 md.push_str("---\n\n");
1238 md.push_str(&hard);
1239 for section in &emitted {
1240 md.push_str(section);
1241 }
1242
1243 if !hints.is_empty() {
1244 md.push_str("## Hints\n\n");
1245 md.push_str(
1246 "_(heavy sections omitted under the token budget — re-query with the key)_\n\n",
1247 );
1248 for (key, tokens) in &hints {
1249 md.push_str(&format!("- `{key}` — estimated_tokens: {tokens}\n"));
1250 }
1251 md.push('\n');
1252 }
1253
1254 if !unknown_includes.is_empty() {
1255 md.push_str("## Warnings\n\n");
1256 for k in &unknown_includes {
1257 md.push_str(&format!(
1258 "- unknown include key `{k}` — allowed: {}\n",
1259 ALLOWED_REPORT_INCLUDE_KEYS.join(", ")
1260 ));
1261 }
1262 md.push('\n');
1263 }
1264
1265 RenderedFidelityReport {
1266 markdown: md,
1267 mode: mode.to_string(),
1268 hints,
1269 budget_used: used,
1270 }
1271}
1272
1273pub fn compute_fidelity_report(
1288 engine: &Engine,
1289 workspace_root: &Path,
1290 binding: &Binding,
1291 resolved: &ResolvedIngest,
1292 key: &FindingKey,
1293) -> FidelityReport {
1294 let binding_id = resolved.name.clone();
1295 let dest = resolved.destination_mem.clone();
1296
1297 let sync_state = engine
1299 .mem_config_for(&dest)
1300 .map(|c| c.sync_state.clone())
1301 .unwrap_or_default();
1302 let mut capabilities: Vec<FacetCapability> = Vec::new();
1303 let mut freshness: Vec<FacetFreshness> = Vec::new();
1304 let mut any_change_detectable = false;
1305 for source in &resolved.sources {
1306 let ResolvedSource::Primary(p) = source else {
1307 continue;
1308 };
1309 let caps = medium_capabilities(p.medium_type);
1310 let medium_type = serde_json::to_value(p.medium_type)
1311 .ok()
1312 .and_then(|v| v.as_str().map(str::to_string))
1313 .unwrap_or_default();
1314 let strategy = resolve_change_strategy(p, workspace_root);
1315 let signal = signal_wire(strategy).to_string();
1316 let signal_readable = match strategy {
1326 ChangeStrategy::Git => {
1327 super::resolve::find_git_root(&super::resolve::source_base_path(p, workspace_root))
1328 .is_some()
1329 }
1330 _ => true,
1331 };
1332 let change_detectable =
1333 caps.change_signal && strategy != ChangeStrategy::None && signal_readable;
1334 any_change_detectable |= change_detectable;
1335
1336 capabilities.push(FacetCapability::from_caps(
1337 p.name.clone(),
1338 medium_type,
1339 caps,
1340 strategy,
1341 ));
1342
1343 let synced = sync_state
1344 .get(&format!("{binding_id}/{}#synced", p.name))
1345 .cloned();
1346 let verified = sync_state
1347 .get(&format!("{binding_id}/{}#verified", p.name))
1348 .cloned();
1349 freshness.push(FacetFreshness {
1350 facet: p.name.clone(),
1351 signal,
1352 synced,
1353 verified,
1354 change_detectable,
1355 });
1356 }
1357
1358 let source_moved_past_synced = if any_change_detectable {
1359 Some(source_moved(engine, resolved, workspace_root))
1360 } else {
1361 None
1362 };
1363
1364 let mut s_d: Vec<String> = Vec::new();
1366 let mut enumerable_facets = 0usize;
1367 let mut empty_enumerable_facets: BTreeSet<String> = BTreeSet::new();
1374 let mut malformed_patterns: Vec<String> = Vec::new();
1379 let mut legacy_patterns: Vec<String> = Vec::new();
1380 let mut partiality_reasons: Vec<String> = Vec::new();
1386 for source in &resolved.sources {
1387 if let ResolvedSource::Primary(p) = source {
1388 let caps = medium_capabilities(p.medium_type);
1389 if caps.enumerable {
1390 enumerable_facets += 1;
1391 }
1392 let walked = enumerate_source_artifacts_reported(
1393 engine,
1394 p,
1395 &resolved.deny_paths,
1396 workspace_root,
1397 );
1398 if caps.enumerable && walked.files.is_empty() {
1399 empty_enumerable_facets.insert(p.name.clone());
1400 }
1401 for m in &walked.malformed {
1402 malformed_patterns.push(format!("`{}` in facet `{}`", m, p.name));
1403 }
1404 for note in &walked.legacy_dialect {
1405 legacy_patterns.push(format!("`{}` in facet `{}`", note.pattern, p.name));
1406 }
1407 if let Some(reason) = walked.partiality_reason() {
1408 partiality_reasons.push(format!("facet `{}`: {reason}", p.name));
1409 }
1410 s_d.extend(walked.files);
1411 }
1412 }
1413 s_d.sort();
1414 s_d.dedup();
1415
1416 let denominator = if !partiality_reasons.is_empty() {
1417 DenominatorBasis::Partial {
1420 count: s_d.len(),
1421 reason: partiality_reasons.join("; "),
1422 }
1423 } else if !s_d.is_empty() {
1424 DenominatorBasis::Enumerated { count: s_d.len() }
1425 } else if enumerable_facets == 0 {
1426 DenominatorBasis::NonEnumerable {
1427 reason: "the medium type(s) are not enumerable this cycle".to_string(),
1428 }
1429 } else if !legacy_patterns.is_empty() {
1430 DenominatorBasis::NonEnumerable {
1435 reason: format!(
1436 "scope pattern(s) still written against the workspace root rather than the \
1437 source pointer, so they select nothing under the pointer join: {}. Rewrite \
1438 them relative to the source's pointer",
1439 legacy_patterns.join(", ")
1440 ),
1441 }
1442 } else {
1443 DenominatorBasis::NonEnumerable {
1447 reason: "no source artifacts enumerated in scope".to_string(),
1448 }
1449 };
1450
1451 let mut direct_covered = 0usize;
1452 let mut tree_only_covered = 0usize;
1453 let mut uncovered: Vec<String> = Vec::new();
1454 let mut describing: BTreeSet<String> = BTreeSet::new();
1455 let mut tree_fanout: BTreeMap<(String, String), usize> = BTreeMap::new();
1456 let entity_end_reconciled = engine.entity_set_is_reconcilable(dest.as_str()).is_ok();
1457 for file in &s_d {
1458 let refs = engine.anchors_referencing_artifact(file);
1472 let mine: Vec<&(crate::EntityId, crate::anchor::Anchor)> = refs
1473 .iter()
1474 .filter(|(eid, a)| {
1475 eid.mem() == dest.as_str()
1476 && a.binding
1477 .as_deref()
1478 .map(|b| b == key.binding_hash.as_str())
1479 .unwrap_or(true)
1480 && (!entity_end_reconciled || !engine.entity_is_absent(eid))
1481 })
1482 .collect();
1483 if mine.is_empty() {
1484 uncovered.push(file.clone());
1485 continue;
1486 }
1487 describing.extend(mine.iter().map(|(eid, _)| eid.as_ref().to_string()));
1488 let has_non_tree = mine.iter().any(|(_, a)| a.grain != AnchorGrain::Tree);
1489 if has_non_tree {
1490 direct_covered += 1;
1491 } else {
1492 tree_only_covered += 1;
1493 }
1494 for (eid, a) in &mine {
1496 if a.grain == AnchorGrain::Tree {
1497 *tree_fanout
1498 .entry((eid.as_ref().to_string(), a.artifact.clone()))
1499 .or_insert(0) += 1;
1500 }
1501 }
1502 }
1503 let tree_anchors: Vec<TreeFanout> = tree_fanout
1504 .into_iter()
1505 .map(|((entity, artifact), fanout)| TreeFanout {
1506 entity,
1507 artifact,
1508 fanout,
1509 })
1510 .collect();
1511
1512 let coverage = GrainCoverage {
1513 denominator,
1514 covered_artifacts: direct_covered + tree_only_covered,
1515 describing_entities: describing.len(),
1516 unit: COVERAGE_UNIT,
1517 direct_covered,
1518 tree_only_covered,
1519 uncovered: uncovered.clone(),
1520 tree_anchors,
1521 };
1522
1523 let population = crate::ingest::anchor_population::population_for(
1527 engine,
1528 resolved,
1529 Some(key.binding_hash.as_str()),
1530 );
1531 let mut anchors = AnchorComposition {
1532 counted_rows: population.included.len(),
1533 distinct_artifacts: population.distinct_artifacts(),
1534 excluded_other_binding: population
1535 .excluded_count(crate::ingest::anchor_population::ExclusionReason::OtherBinding),
1536 excluded_out_of_scope: population
1537 .excluded_count(crate::ingest::anchor_population::ExclusionReason::OutOfScope),
1538 excluded_artifacts: population
1539 .excluded
1540 .iter()
1541 .map(|e| format!("{} ({})", e.artifact, e.reason.as_wire()))
1542 .collect(),
1543 counted_without_provenance: population.without_provenance,
1544 dangling: population.dangling.len(),
1545 dangling_rows: population
1546 .dangling
1547 .iter()
1548 .map(|d| format!("{} → {}", d.entity, d.artifact))
1549 .collect(),
1550 unreconciled: population.unreconciled.map(str::to_string),
1551 span_unvalidated: population
1552 .included
1553 .iter()
1554 .filter(|(_, r)| r.anchor.span_unvalidated)
1555 .count(),
1556 hash_from_backfill: population
1557 .included
1558 .iter()
1559 .filter(|(_, r)| {
1560 r.anchor.hash_source == Some(crate::anchor::AnchorHashSource::Backfill)
1561 })
1562 .count(),
1563 aging: {
1564 let today = crate::engine::mutation::iso_now();
1565 let mut rows: Vec<AgingAnchor> = population
1566 .included
1567 .iter()
1568 .filter_map(|(eid, r)| {
1569 let at = r.observed_at.as_deref()?;
1570 Some(AgingAnchor {
1571 entity: eid.as_ref().to_string(),
1572 artifact: r.anchor.artifact.clone(),
1573 observed_at: at.to_string(),
1574 unobserved_for_days: crate::anchor::days_between(at, &today).unwrap_or(0),
1575 })
1576 })
1577 .collect();
1578 rows.sort_by_key(|a| std::cmp::Reverse(a.unobserved_for_days));
1579 rows
1580 },
1581 ..Default::default()
1582 };
1583 for (_eid, resolved_anchor) in population.included {
1584 let a = &resolved_anchor.anchor;
1585 *anchors
1586 .by_class
1587 .entry(a.class.as_wire().to_string())
1588 .or_insert(0) += 1;
1589 *anchors
1590 .by_grain
1591 .entry(a.grain.as_wire().to_string())
1592 .or_insert(0) += 1;
1593 if a.class == AnchorProvenanceClass::Authored {
1594 anchors.authored += 1;
1595 continue; }
1597 match resolved_anchor.state {
1598 Some(AnchorState::Resolves) => {
1599 anchors.resolves += 1;
1600 anchors.observed += 1;
1601 }
1602 Some(AnchorState::Drifted) => {
1603 anchors.drifted += 1;
1604 anchors.observed += 1;
1605 }
1606 Some(AnchorState::Recheck) => {
1607 anchors.recheck += 1;
1608 anchors.observed += 1;
1609 }
1610 Some(AnchorState::Orphaned) => {
1611 anchors.orphaned += 1;
1612 anchors.observed += 1;
1613 }
1614 None => anchors.unobserved += 1,
1615 }
1616 }
1617
1618 let mut findings_by_class: BTreeMap<String, usize> = BTreeMap::new();
1620 let mut backlog = 0usize;
1621 let mut superseded: Vec<String> = Vec::new();
1622 if let Some((mem, name)) = binding_id.split_once('/')
1623 && let Ok(Some(store)) = read_findings_store(workspace_root, mem, name)
1624 {
1625 for f in store.current(key) {
1626 *findings_by_class
1627 .entry(f.class.as_wire().to_string())
1628 .or_insert(0) += 1;
1629 if f.class == FindingClass::QueuedForAdjudication {
1630 backlog += 1;
1631 }
1632 }
1633 for f in store.superseded(key) {
1634 superseded.push(format!(
1635 "[{}] {} ({})",
1636 f.class.as_wire(),
1637 finding_target_label(&f.target),
1638 f.facet
1639 ));
1640 }
1641 }
1642
1643 let mut disposed_excluded_rationales: Vec<(String, String)> = Vec::new();
1649 if let Some((mem, name)) = binding_id.split_once('/')
1650 && let Ok(Some(state)) = read_advance_store(workspace_root, mem, name)
1651 {
1652 let uncovered_set: std::collections::BTreeSet<&str> =
1653 uncovered.iter().map(String::as_str).collect();
1654 for (artifact, rationale) in &state.exclusions {
1655 if uncovered_set.contains(artifact.as_str()) {
1656 disposed_excluded_rationales.push((artifact.clone(), rationale.clone()));
1657 }
1658 }
1659 }
1660 let disposed_excluded = disposed_excluded_rationales.len();
1661
1662 let mut degradations: Vec<String> = Vec::new();
1664 for c in &capabilities {
1665 if !c.change_signal || c.signal == "none" {
1666 degradations.push(format!(
1667 "change-signal-none:`{}` — freshness is unknowable for this facet",
1668 c.facet
1669 ));
1670 }
1671 if !c.enumerable {
1672 degradations.push(format!(
1673 "enumeration-unavailable:`{}` — `S(D)` coverage denominator not computable",
1674 c.facet
1675 ));
1676 } else if empty_enumerable_facets.contains(&c.facet) {
1677 degradations.push(format!(
1685 "enumeration-empty:`{}` — the medium claims enumerability but the walk yielded \
1686 no artifacts; coverage is reported over anchors only",
1687 c.facet
1688 ));
1689 }
1690 if !c.base_version_retrievable {
1691 degradations.push(format!(
1692 "base-version-unretrievable:`{}` — prune degrades to conflict-flagging",
1693 c.facet
1694 ));
1695 }
1696 }
1697 if anchors.recheck > 0 {
1698 degradations.push(format!(
1699 "hash-adjudication-deferred — {} anchor(s) recheck (unstable medium / hash \
1700 unavailable), not asserted drift",
1701 anchors.recheck
1702 ));
1703 }
1704 if anchors.unobserved > 0 {
1705 degradations.push(format!(
1706 "anchors-unobserved — {} anchor(s) could not be observed this pass",
1707 anchors.unobserved
1708 ));
1709 }
1710
1711 let adopt = super::render::mem_predates_binding(engine, resolved);
1715 let effective_coverage = crate::binding::effective_coverage_semantics(binding);
1716
1717 FidelityReport {
1718 legacy_dialect_patterns: legacy_patterns,
1719 binding: binding_id,
1720 destination_mem: dest,
1721 adopt,
1722 coverage_semantics: effective_coverage.value,
1723 coverage_semantics_declared: effective_coverage.declared,
1724 capabilities,
1725 freshness,
1726 source_moved_past_synced,
1727 coverage,
1728 anchors,
1729 findings_by_class,
1730 backlog,
1731 superseded,
1732 disposed_excluded,
1733 disposed_excluded_rationales,
1734 degradations,
1735 }
1736}
1737
1738fn strategy_retrieves_base(strategy: ChangeStrategy) -> bool {
1747 matches!(strategy, ChangeStrategy::Git | ChangeStrategy::Graph)
1748}
1749
1750fn signal_wire(strategy: ChangeStrategy) -> &'static str {
1753 match strategy {
1754 ChangeStrategy::None => "none",
1755 ChangeStrategy::Git => "git",
1756 ChangeStrategy::Mtime => "mtime",
1757 ChangeStrategy::Graph => "graph",
1758 }
1759}
1760
1761fn finding_target_label(target: &super::findings::FindingTarget) -> String {
1763 match target {
1764 super::findings::FindingTarget::Anchor { entity, artifact } => {
1765 format!("{entity} → {artifact}")
1766 }
1767 super::findings::FindingTarget::Artifact { artifact } => artifact.clone(),
1768 }
1769}
1770
1771#[cfg(test)]
1772mod tests {
1773 use super::*;
1774
1775 fn base_report() -> FidelityReport {
1778 FidelityReport {
1779 legacy_dialect_patterns: Vec::new(),
1780 binding: "engine/graph".to_string(),
1781 destination_mem: "engine".to_string(),
1782 adopt: false,
1783 coverage_semantics: CoverageSemantics::Exhaustive,
1784 coverage_semantics_declared: true,
1785 capabilities: vec![FacetCapability {
1786 facet: "src".to_string(),
1787 medium_type: "codebase".to_string(),
1788 enumerable: true,
1789 change_signal: true,
1790 base_version_retrievable: true,
1791 anchor_namespace: "path".to_string(),
1792 signal: "git".to_string(),
1793 }],
1794 freshness: vec![FacetFreshness {
1795 facet: "src".to_string(),
1796 signal: "git".to_string(),
1797 synced: Some("deadbeef".to_string()),
1798 verified: None,
1799 change_detectable: true,
1800 }],
1801 source_moved_past_synced: Some(false),
1802 coverage: GrainCoverage {
1803 denominator: DenominatorBasis::Enumerated { count: 10 },
1804 covered_artifacts: 9,
1805 describing_entities: 4,
1806 unit: COVERAGE_UNIT,
1807 direct_covered: 6,
1808 tree_only_covered: 3,
1809 uncovered: vec!["src/a.rs".to_string()],
1810 tree_anchors: vec![TreeFanout {
1811 entity: "engine--big".to_string(),
1812 artifact: "src/".to_string(),
1813 fanout: 3,
1814 }],
1815 },
1816 anchors: AnchorComposition {
1817 by_class: BTreeMap::from([
1818 ("anchored".to_string(), 5),
1819 ("authored".to_string(), 2),
1820 ]),
1821 by_grain: BTreeMap::from([("file".to_string(), 4), ("tree".to_string(), 1)]),
1822 authored: 2,
1823 observed: 5,
1824 resolves: 4,
1825 drifted: 0,
1826 recheck: 1,
1827 orphaned: 0,
1828 unobserved: 0,
1829 ..Default::default()
1830 },
1831 findings_by_class: BTreeMap::from([
1832 ("uncovered".to_string(), 1),
1833 ("queued-for-adjudication".to_string(), 1),
1834 ]),
1835 backlog: 1,
1836 superseded: Vec::new(),
1837 disposed_excluded: 0,
1838 disposed_excluded_rationales: Vec::new(),
1839 degradations: vec!["hash-adjudication-deferred — 1 anchor(s) recheck".to_string()],
1840 }
1841 }
1842
1843 #[test]
1846 fn aging_rows_render_with_their_age() {
1847 let mut r = base_report();
1848 r.anchors.aging = vec![AgingAnchor {
1849 entity: "engine--cites".to_string(),
1850 artifact: "https://w.test/living".to_string(),
1851 observed_at: "2026-08-03T09:00:00Z".to_string(),
1852 unobserved_for_days: 30,
1853 }];
1854 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
1855 assert!(
1856 md.contains("1 counted row(s) rest on a recorded observation"),
1857 "{md}"
1858 );
1859 assert!(
1860 md.contains(
1861 "`engine--cites` → `https://w.test/living`: unobserved for 30 days (observed 2026-08-03T09:00:00Z)"
1862 ),
1863 "{md}"
1864 );
1865 let plain = render_fidelity_report(&base_report(), 8_000, &[]).markdown;
1866 assert!(!plain.contains("recorded observation"), "{plain}");
1867 }
1868
1869 #[test]
1874 fn b1_renders_all_elements_deterministically() {
1875 let r = base_report();
1876 let a = render_fidelity_report(&r, 8_000, &[]);
1877 let b = render_fidelity_report(&r, 8_000, &[]);
1878 assert_eq!(a.markdown, b.markdown, "deterministic — identical bytes");
1879
1880 let md = &a.markdown;
1881 assert!(md.contains("direct-covered (file / span anchors): 6/10"));
1883 assert!(md.contains(
1884 "tree-anchor fan-out (separate axis): 1 tree anchor(s) fanning out over 3 file(s)"
1885 ));
1886 assert!(
1888 !md.contains("9/10"),
1889 "tree fan-out must not blend into direct coverage"
1890 );
1891 assert!(md.contains("anchor-resolution %:** 4/5"));
1893 assert!(md.contains("`authored` bucket (excluded from coverage/accuracy denominators): 2"));
1895 assert!(md.contains("tier-3 adjudication backlog:** 1"));
1897 assert!(md.contains("## Capability matrix"));
1899 assert!(md.contains("## Degradations"));
1900 assert!(md.contains("hash-adjudication-deferred"));
1901 assert!(md.contains("per-medium enumeration `S(D)` = **10**"));
1903 }
1904
1905 #[test]
1908 fn b2_detectionless_medium_freshness_unknowable_never_green() {
1909 let mut r = base_report();
1910 r.capabilities = vec![FacetCapability {
1911 facet: "manual".to_string(),
1912 medium_type: "web".to_string(),
1913 enumerable: false,
1914 change_signal: false,
1915 base_version_retrievable: false,
1916 anchor_namespace: "url".to_string(),
1917 signal: "none".to_string(),
1918 }];
1919 r.freshness = vec![FacetFreshness {
1920 facet: "manual".to_string(),
1921 signal: "none".to_string(),
1922 synced: Some("should-never-render-green".to_string()),
1925 verified: Some("nor-this".to_string()),
1926 change_detectable: false,
1927 }];
1928 r.source_moved_past_synced = None;
1929 let out = render_fidelity_report(&r, 8_000, &[]);
1930 let md = &out.markdown;
1931 assert!(md.contains("signal: `none`"));
1932 assert!(md.contains("freshness unknowable"));
1933 assert!(!md.contains("should-never-render-green"));
1936 assert!(
1937 !md.contains("`#synced`: `"),
1938 "no synced token rendered for a non-detectable medium"
1939 );
1940 assert!(
1941 !md.contains("at its `#synced` baseline"),
1942 "no green 'at baseline' verdict"
1943 );
1944 }
1945
1946 #[test]
1954 fn b1_base_retrievability_follows_resolved_strategy_not_medium_ceiling() {
1955 use crate::pipeline::MediumType;
1956
1957 assert!(medium_capabilities(MediumType::Filesystem).base_version_retrievable);
1959
1960 let fs_mtime = FacetCapability::from_caps(
1962 "prose".to_string(),
1963 "filesystem".to_string(),
1964 medium_capabilities(MediumType::Filesystem),
1965 ChangeStrategy::Mtime,
1966 );
1967 assert!(
1968 !fs_mtime.base_version_retrievable,
1969 "filesystem+mtime has no retrievable base leg — degrades to conflict-flag"
1970 );
1971 assert_eq!(fs_mtime.signal, "mtime");
1972
1973 let fs_git = FacetCapability::from_caps(
1974 "prose".to_string(),
1975 "filesystem".to_string(),
1976 medium_capabilities(MediumType::Filesystem),
1977 ChangeStrategy::Git,
1978 );
1979 assert!(
1980 fs_git.base_version_retrievable,
1981 "filesystem backed by git keeps the never-clobber base leg"
1982 );
1983
1984 assert!(!strategy_retrieves_base(ChangeStrategy::None));
1986 assert!(!strategy_retrieves_base(ChangeStrategy::Mtime));
1987 assert!(strategy_retrieves_base(ChangeStrategy::Git));
1988 assert!(strategy_retrieves_base(ChangeStrategy::Graph));
1989
1990 let mut r = base_report();
1994 r.capabilities = vec![fs_mtime.clone()];
1995 r.degradations = if !fs_mtime.base_version_retrievable {
1996 vec![format!(
1997 "base-version-unretrievable:`{}` — prune degrades to conflict-flagging",
1998 fs_mtime.facet
1999 )]
2000 } else {
2001 Vec::new()
2002 };
2003 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
2004 assert!(
2005 md.contains("base-version-unretrievable:`prose` — prune degrades to conflict-flagging"),
2006 "filesystem+mtime surfaces the conflict-flag degradation in the report"
2007 );
2008 }
2009
2010 #[test]
2013 fn b3_aggregates_always_ship_at_zero_budget() {
2014 let r = base_report();
2015 let out = render_fidelity_report(&r, 0, &[]);
2016 assert_eq!(out.mode, "overbudget");
2017 let md = &out.markdown;
2018 assert!(md.contains("direct-covered (file / span anchors): 6/10"));
2020 assert!(md.contains("tier-3 adjudication backlog:** 1"));
2021 assert!(md.contains("## Capability matrix"));
2022 assert!(!md.contains("## Uncovered artifacts"));
2024 assert!(md.contains("## Hints"));
2025 assert!(out.hints.iter().any(|(k, _)| k == "uncovered_artifacts"));
2026 }
2027
2028 #[test]
2032 fn b3_large_facet_list_truncates_then_include_forces() {
2033 let mut r = base_report();
2034 r.coverage.uncovered = (0..500).map(|i| format!("src/file_{i}.rs")).collect();
2036 let hard_cost = estimate_tokens(&render_hard_required(&r));
2038 let out = render_fidelity_report(&r, hard_cost + 5, &[]);
2039 assert_eq!(out.mode, "reduced");
2040 assert!(
2041 !out.markdown.contains("src/file_499.rs"),
2042 "big list not rendered unbounded"
2043 );
2044 assert!(out.markdown.contains("## Hints"));
2045 let (_, est) = out
2046 .hints
2047 .iter()
2048 .find(|(k, _)| k == "uncovered_artifacts")
2049 .expect("uncovered list hinted");
2050 assert!(*est > 5, "the hint carries a real estimated_tokens figure");
2051
2052 let forced =
2054 render_fidelity_report(&r, hard_cost + 5, &["uncovered_artifacts".to_string()]);
2055 assert!(
2056 forced.markdown.contains("src/file_499.rs"),
2057 "include forces the full list"
2058 );
2059 }
2060
2061 #[test]
2064 fn b4_curated_vs_exhaustive_framing() {
2065 let mut exhaustive = base_report();
2066 exhaustive.coverage_semantics = CoverageSemantics::Exhaustive;
2067 let ex_md = render_fidelity_report(&exhaustive, 8_000, &[]).markdown;
2068 assert!(ex_md.contains("Exhaustive coverage:"));
2069 assert!(ex_md.contains("are **findings**"));
2070
2071 let mut curated = base_report();
2072 curated.coverage_semantics = CoverageSemantics::Curated;
2073 let cur_md = render_fidelity_report(&curated, 8_000, &[]).markdown;
2074 assert!(cur_md.contains("Curated coverage:"));
2075 assert!(cur_md.contains("**information**"));
2076 assert!(
2077 !cur_md.contains("are **findings**"),
2078 "curated never frames unaccounted as findings"
2079 );
2080 }
2081
2082 #[test]
2085 fn b4_disposition_excludes_from_exhaustive_findings() {
2086 let mut r = base_report();
2087 r.coverage_semantics = CoverageSemantics::Exhaustive;
2088 r.coverage.uncovered = vec!["src/a.rs".to_string(), "src/b.rs".to_string()];
2089 r.disposed_excluded = 1;
2090 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
2091 assert!(md.contains("1 unaccounted artifact(s)"));
2093 assert!(md.contains("(1 disposed excluded)"));
2094 }
2095
2096 #[test]
2099 fn b4_authored_exclusion_rationale_is_rendered() {
2100 let mut r = base_report();
2101 r.coverage_semantics = CoverageSemantics::Exhaustive;
2102 r.coverage.uncovered = vec!["src/gen.rs".to_string()];
2103 r.disposed_excluded = 1;
2104 r.disposed_excluded_rationales =
2105 vec![("src/gen.rs".to_string(), "generated; no entity".to_string())];
2106 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
2107 assert!(md.contains("Excluded on purpose (persisted dispositions):"));
2108 assert!(md.contains("`src/gen.rs` — generated; no entity"));
2109 }
2110
2111 #[test]
2114 fn b5_denominator_provenance_stated() {
2115 let r = base_report();
2116 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
2117 assert!(md.contains("## Denominator provenance"));
2118 assert!(md.contains("per-medium enumeration `S(D)` = **10**"));
2119
2120 let mut non = base_report();
2121 non.coverage.denominator = DenominatorBasis::NonEnumerable {
2122 reason: "the medium type(s) are not enumerable this cycle".to_string(),
2123 };
2124 let md2 = render_fidelity_report(&non, 8_000, &[]).markdown;
2125 assert!(md2.contains("No `S(D)` denominator"));
2126 assert!(md2.contains("not enumerable this cycle"));
2127 }
2128
2129 #[test]
2135 fn e1_adopt_report_renders_onboarding_no_red_verdict() {
2136 let mut r = base_report();
2137 r.adopt = true;
2138 r.coverage_semantics = CoverageSemantics::Exhaustive;
2139 r.coverage.uncovered = (0..5).map(|i| format!("src/file_{i}.rs")).collect();
2140 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
2141
2142 assert!(md.contains("## Adopting — first verify"));
2144 assert!(md.contains("0% anchored is expected — this is onboarding, not a failure."));
2145 assert!(
2147 md.contains("**Backfill path:** run `memstead projection brief engine/graph --sync`")
2148 );
2149 assert!(
2152 !md.contains("are **findings**"),
2153 "pre-binding history must not produce a red findings verdict"
2154 );
2155 assert!(md.contains("Exhaustive coverage (onboarding):"));
2156 assert!(md.contains("backfill worklist"));
2157
2158 r.adopt = false;
2160 let md2 = render_fidelity_report(&r, 8_000, &[]).markdown;
2161 assert!(!md2.contains("## Adopting — first verify"));
2162 assert!(md2.contains("are **findings**"));
2163 }
2164
2165 #[test]
2167 fn unknown_include_key_warns() {
2168 let r = base_report();
2169 let out = render_fidelity_report(&r, 8_000, &["bogus".to_string()]);
2170 assert!(out.markdown.contains("unknown include key `bogus`"));
2171 }
2172
2173 use crate::anchor::{Anchor, AnchorHashStability, AnchorProvenanceClass, AnchorSidecar};
2176 use crate::binding::{
2177 BINDING_VERSION, Binding, BuildMode, BuildOperation, DEFAULT_ADJUDICATION_CAP,
2178 DEFAULT_FULL_RESYNC_EVERY, Operations, VerifyOperation,
2179 };
2180 use crate::ingest::findings::verify_binding;
2181 use crate::ingest::resolve::resolve_binding_run;
2182 use crate::pipeline::{IngestTrigger, MediumType, PatternEntry, PatternMode};
2183 use crate::pipeline_store::{load_pipeline_configs, write_binding};
2184 use crate::workspace::{
2185 Mount, MountCapability, MountLifecycle, MountStorage, Workspace, WorkspaceSettings,
2186 };
2187 use crate::workspace_store::WorkspaceStoreAdapter;
2188
2189 #[test]
2196 fn compute_report_end_to_end() {
2197 let tmp = tempfile::tempdir().unwrap();
2198 let (report, outcome, md) = end_to_end_report(tmp.path(), &["direct", "tree", "auth"]);
2199 end_to_end_body(&report, &outcome, &md);
2200 }
2201
2202 #[test]
2207 fn coverage_does_not_rest_on_an_anchor_whose_entity_is_gone() {
2208 let tmp = tempfile::tempdir().unwrap();
2209 let (report, _outcome, md) = end_to_end_report(tmp.path(), &["tree"]);
2210 assert_eq!(
2211 report.coverage.direct_covered, 0,
2212 "the only direct anchor on present.rs is dangling, so nothing covers it directly"
2213 );
2214 assert!(
2215 report
2216 .coverage
2217 .uncovered
2218 .contains(&"src/present.rs".to_string()),
2219 "and the artifact reads uncovered rather than covered by a phantom"
2220 );
2221 assert_eq!(report.anchors.dangling, 2);
2223 assert_eq!(report.anchors.counted_rows, 1);
2224 assert_eq!(report.anchors.unreconciled, None);
2225 assert!(
2226 md.contains("name an entity this mem no longer holds"),
2227 "and the report says so on the page, not only in the struct"
2228 );
2229 }
2230
2231 fn end_to_end_report(
2237 root: &std::path::Path,
2238 entity_slugs: &[&str],
2239 ) -> (
2240 FidelityReport,
2241 crate::ingest::findings::VerifyOutcome,
2242 String,
2243 ) {
2244 let mem_dir = root.join("mem");
2245 std::fs::create_dir_all(mem_dir.join(".memstead")).unwrap();
2246 std::fs::write(
2247 mem_dir.join(".memstead").join("config.json"),
2248 r#"{"format":1,"schema":"default@1.0.0","version":"1.0.0"}"#,
2249 )
2250 .unwrap();
2251
2252 std::fs::create_dir_all(root.join(".memstead")).unwrap();
2253 std::fs::write(
2254 root.join(".memstead").join("workspace.toml"),
2255 "format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
2256 )
2257 .unwrap();
2258 let mount = Mount {
2259 mem: "engine".to_string(),
2260 schema: Some("default@1.0.0".parse().unwrap()),
2261 storage: MountStorage::Folder {
2262 path: mem_dir.clone(),
2263 },
2264 capability: MountCapability::Write,
2265 lifecycle: MountLifecycle::Eager,
2266 cross_linkable: false,
2267 migration_target: None,
2268 };
2269 crate::FileWorkspaceStore::new()
2270 .save_state(
2271 root,
2272 &Workspace {
2273 mounts: vec![mount],
2274 settings: WorkspaceSettings::default(),
2275 },
2276 )
2277 .unwrap();
2278
2279 let out = std::process::Command::new("git")
2280 .args(["init", "-q"])
2281 .current_dir(root)
2282 .output()
2283 .unwrap();
2284 assert!(out.status.success());
2285 std::fs::create_dir_all(root.join("src").join("sub")).unwrap();
2286 std::fs::write(root.join("src").join("present.rs"), "fn a() {}\n").unwrap();
2287 std::fs::write(root.join("src").join("uncovered.rs"), "fn b() {}\n").unwrap();
2288 std::fs::write(root.join("src").join("sub").join("deep.rs"), "fn c() {}\n").unwrap();
2289
2290 let mk = |artifact: &str, grain: AnchorGrain, class: AnchorProvenanceClass| Anchor {
2291 artifact: artifact.to_string(),
2292 grain,
2293 class,
2294 at_version: None,
2295 hash: class.is_hash_bearing().then(|| "recorded".to_string()),
2296 hash_stability: AnchorHashStability::Stable,
2297 derived_from: Vec::new(),
2298 binding: None,
2299 source: None,
2300 span_unvalidated: false,
2301 hash_source: None,
2302 last_observed: None,
2303 };
2304 for slug in entity_slugs {
2308 std::fs::write(
2309 mem_dir.join(format!("{slug}.md")),
2310 "---\ntype: decision\n---\n\n# E\n\n## Decision\n\nBody.\n",
2311 )
2312 .unwrap();
2313 }
2314 let mut sidecar = AnchorSidecar::default();
2315 sidecar.set(
2316 "engine--direct",
2317 vec![mk(
2318 "src/present.rs",
2319 AnchorGrain::File,
2320 AnchorProvenanceClass::Anchored,
2321 )],
2322 );
2323 sidecar.set(
2324 "engine--tree",
2325 vec![mk(
2326 "src/sub/",
2327 AnchorGrain::Tree,
2328 AnchorProvenanceClass::Anchored,
2329 )],
2330 );
2331 sidecar.set(
2333 "engine--auth",
2334 vec![mk(
2335 "src/present.rs",
2336 AnchorGrain::File,
2337 AnchorProvenanceClass::Authored,
2338 )],
2339 );
2340 std::fs::write(
2341 mem_dir.join(crate::anchor::ANCHOR_SIDECAR_PATH),
2342 sidecar.to_bytes(),
2343 )
2344 .unwrap();
2345
2346 write_binding(
2347 root,
2348 "engine",
2349 "graph",
2350 &Binding {
2351 version: BINDING_VERSION,
2352 intent: None,
2353 sources: vec![crate::pipeline::Source {
2354 name: "graph".to_string(),
2355 medium_type: MediumType::Codebase,
2356 pointer: String::new(),
2357 change_detection: Some("git".to_string()),
2358 scope: vec![PatternEntry {
2359 path: "src/**/*.rs".to_string(),
2360 mode: PatternMode::Allow,
2361 }],
2362 engagement: None,
2363 preparation: None,
2364 }],
2365 reference_mems: Vec::new(),
2366 destination_mem: "engine".to_string(),
2367 deny_paths: Vec::new(),
2368 coverage_semantics: None,
2369 rules: None,
2370 prune: None,
2371 operations: Operations {
2372 build: Some(BuildOperation {
2373 mode: BuildMode::Discovery,
2374 trigger: IngestTrigger::Loop,
2375 batch_size: 20,
2376 post_actions: None,
2377 }),
2378 sync: None,
2379 verify: Some(VerifyOperation {
2380 trigger: IngestTrigger::Manual,
2381 batch_size: 20,
2382 adjudication_cap: DEFAULT_ADJUDICATION_CAP,
2383 full_resync_every: DEFAULT_FULL_RESYNC_EVERY,
2384 }),
2385 },
2386 },
2387 )
2388 .unwrap();
2389
2390 let engine = Engine::from_workspace_root(root).unwrap();
2391 let configs = load_pipeline_configs(root).unwrap();
2392 let binding = &configs.bindings[0].config;
2393 let resolved = resolve_binding_run("engine/graph", binding).unwrap();
2394
2395 let outcome = verify_binding(&engine, root, binding, &resolved).unwrap();
2397
2398 let report = compute_fidelity_report(&engine, root, binding, &resolved, &outcome.key);
2400 let md = render_fidelity_report(&report, 8_000, &[]).markdown;
2401 (report, outcome, md)
2402 }
2403
2404 fn end_to_end_body(
2405 report: &FidelityReport,
2406 outcome: &crate::ingest::findings::VerifyOutcome,
2407 md: &str,
2408 ) {
2409 assert_eq!(
2411 report.coverage.denominator,
2412 DenominatorBasis::Enumerated { count: 3 }
2413 );
2414 assert_eq!(report.coverage.direct_covered, 1);
2417 assert_eq!(report.coverage.tree_only_covered, 1);
2418 assert_eq!(
2419 report.coverage.uncovered,
2420 vec!["src/uncovered.rs".to_string()]
2421 );
2422 assert_eq!(report.coverage.tree_anchors.len(), 1);
2424 assert_eq!(report.coverage.tree_anchors[0].fanout, 1);
2425 assert_eq!(report.coverage.tree_anchors[0].artifact, "src/sub/");
2426 assert_eq!(report.anchors.authored, 1);
2428 assert_eq!(report.anchors.by_class.get("authored"), Some(&1));
2429 assert_eq!(report.anchors.observed, 2);
2434 assert_eq!(report.anchors.recheck, 1);
2435 assert_eq!(report.anchors.drifted, 1);
2436 assert_eq!(report.backlog, outcome.backlog);
2438 assert!(
2440 report
2441 .degradations
2442 .iter()
2443 .any(|d| d.contains("hash-adjudication-deferred"))
2444 );
2445 assert!(md.contains("per-medium enumeration `S(D)` = **3**"));
2447 assert!(!report.adopt);
2450 assert!(!md.contains("## Adopting — first verify"));
2451 }
2452
2453 #[test]
2458 fn compute_report_adopt_when_mem_predates_binding() {
2459 let tmp = tempfile::tempdir().unwrap();
2460 let root = tmp.path();
2461 let mem_dir = root.join("mem");
2462 std::fs::create_dir_all(mem_dir.join(".memstead")).unwrap();
2463 std::fs::write(
2464 mem_dir.join(".memstead").join("config.json"),
2465 r#"{"format":1,"schema":"default@1.0.0","version":"1.0.0"}"#,
2466 )
2467 .unwrap();
2468 std::fs::create_dir_all(root.join(".memstead")).unwrap();
2469 std::fs::write(
2470 root.join(".memstead").join("workspace.toml"),
2471 "format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
2472 )
2473 .unwrap();
2474 let mount = Mount {
2475 mem: "engine".to_string(),
2476 schema: Some("default@1.0.0".parse().unwrap()),
2477 storage: MountStorage::Folder {
2478 path: mem_dir.clone(),
2479 },
2480 capability: MountCapability::Write,
2481 lifecycle: MountLifecycle::Eager,
2482 cross_linkable: false,
2483 migration_target: None,
2484 };
2485 crate::FileWorkspaceStore::new()
2486 .save_state(
2487 root,
2488 &Workspace {
2489 mounts: vec![mount],
2490 settings: WorkspaceSettings::default(),
2491 },
2492 )
2493 .unwrap();
2494 let out = std::process::Command::new("git")
2495 .args(["init", "-q"])
2496 .current_dir(root)
2497 .output()
2498 .unwrap();
2499 assert!(out.status.success());
2500 std::fs::create_dir_all(root.join("src")).unwrap();
2501 std::fs::write(root.join("src").join("a.rs"), "fn a() {}\n").unwrap();
2503 std::fs::write(root.join("src").join("b.rs"), "fn b() {}\n").unwrap();
2504
2505 write_binding(
2506 root,
2507 "engine",
2508 "graph",
2509 &Binding {
2510 version: BINDING_VERSION,
2511 intent: None,
2512 sources: vec![crate::pipeline::Source {
2513 name: "graph".to_string(),
2514 medium_type: MediumType::Codebase,
2515 pointer: String::new(),
2516 change_detection: Some("git".to_string()),
2517 scope: vec![PatternEntry {
2518 path: "src/**/*.rs".to_string(),
2519 mode: PatternMode::Allow,
2520 }],
2521 engagement: None,
2522 preparation: None,
2523 }],
2524 reference_mems: Vec::new(),
2525 destination_mem: "engine".to_string(),
2526 deny_paths: Vec::new(),
2527 coverage_semantics: None,
2528 rules: None,
2529 prune: None,
2530 operations: Operations {
2531 build: Some(BuildOperation {
2532 mode: BuildMode::Discovery,
2533 trigger: IngestTrigger::Loop,
2534 batch_size: 20,
2535 post_actions: None,
2536 }),
2537 sync: None,
2538 verify: Some(VerifyOperation {
2539 trigger: IngestTrigger::Manual,
2540 batch_size: 20,
2541 adjudication_cap: DEFAULT_ADJUDICATION_CAP,
2542 full_resync_every: DEFAULT_FULL_RESYNC_EVERY,
2543 }),
2544 },
2545 },
2546 )
2547 .unwrap();
2548
2549 let engine = Engine::from_workspace_root(root).unwrap();
2550 let configs = load_pipeline_configs(root).unwrap();
2551 let binding = &configs.bindings[0].config;
2552 let resolved = resolve_binding_run("engine/graph", binding).unwrap();
2553 let outcome = verify_binding(&engine, root, binding, &resolved).unwrap();
2554 let report = compute_fidelity_report(&engine, root, binding, &resolved, &outcome.key);
2555
2556 assert!(
2558 report.adopt,
2559 "a no-anchor, never-synced mem predates its binding"
2560 );
2561 let md = render_fidelity_report(&report, 8_000, &[]).markdown;
2562 assert!(md.contains("## Adopting — first verify"));
2563 assert!(md.contains("0% anchored is expected"));
2564 assert!(!md.contains("are **findings**"));
2566 assert!(md.contains("Exhaustive coverage (onboarding):"));
2567 }
2568
2569 #[test]
2573 fn report_marks_resolved_coverage_semantics() {
2574 let mut resolved = base_report();
2575 resolved.coverage_semantics = CoverageSemantics::Curated;
2576 resolved.coverage_semantics_declared = false;
2577 let md = render_hard_required(&resolved);
2578 assert!(
2579 md.contains("curated (resolved from the sources' media — not declared)"),
2580 "resolved value carries the marker: {md}"
2581 );
2582
2583 let declared = base_report(); let md = render_hard_required(&declared);
2585 assert!(
2586 md.contains("**Coverage semantics:** exhaustive\n"),
2587 "declared value renders bare: {md}"
2588 );
2589 assert!(
2590 !md.contains("(resolved from the sources' media"),
2591 "no resolution marker on a declared value: {md}"
2592 );
2593 }
2594}
2595
2596#[cfg(test)]
2597mod rollup_tests {
2598 use super::*;
2599
2600 fn clean_report() -> FidelityReport {
2604 FidelityReport {
2605 legacy_dialect_patterns: Vec::new(),
2606 binding: "engine/graph".to_string(),
2607 destination_mem: "engine".to_string(),
2608 adopt: false,
2609 coverage_semantics: CoverageSemantics::Exhaustive,
2610 coverage_semantics_declared: true,
2611 capabilities: vec![FacetCapability {
2612 facet: "src".to_string(),
2613 medium_type: "codebase".to_string(),
2614 enumerable: true,
2615 change_signal: true,
2616 base_version_retrievable: true,
2617 anchor_namespace: "path".to_string(),
2618 signal: "git".to_string(),
2619 }],
2620 freshness: vec![FacetFreshness {
2621 facet: "src".to_string(),
2622 signal: "git".to_string(),
2623 synced: Some("deadbeef".to_string()),
2624 verified: None,
2625 change_detectable: true,
2626 }],
2627 source_moved_past_synced: Some(false),
2628 coverage: GrainCoverage {
2629 denominator: DenominatorBasis::Enumerated { count: 4 },
2630 covered_artifacts: 4,
2631 describing_entities: 2,
2632 unit: COVERAGE_UNIT,
2633 direct_covered: 4,
2634 tree_only_covered: 0,
2635 uncovered: Vec::new(),
2636 tree_anchors: Vec::new(),
2637 },
2638 anchors: AnchorComposition {
2639 by_class: BTreeMap::from([("anchored".to_string(), 4)]),
2640 by_grain: BTreeMap::from([("file".to_string(), 4)]),
2641 authored: 0,
2642 observed: 4,
2643 resolves: 4,
2644 drifted: 0,
2645 recheck: 0,
2646 orphaned: 0,
2647 unobserved: 0,
2648 ..Default::default()
2649 },
2650 findings_by_class: BTreeMap::new(),
2651 backlog: 0,
2652 superseded: Vec::new(),
2653 disposed_excluded: 0,
2654 disposed_excluded_rationales: Vec::new(),
2655 degradations: Vec::new(),
2656 }
2657 }
2658
2659 #[test]
2665 fn unadjudicated_rows_block_clean_but_exclusions_do_not() {
2666 let mut r = clean_report();
2667 assert_eq!(
2668 r.rollup().verdict,
2669 RollupVerdict::Clean,
2670 "the baseline is clean"
2671 );
2672
2673 r.anchors.excluded_out_of_scope = 3;
2675 r.anchors.excluded_other_binding = 2;
2676 r.anchors.excluded_artifacts = vec!["src/a.rs (out-of-scope)".into()];
2677 assert_eq!(
2678 r.rollup().verdict,
2679 RollupVerdict::Clean,
2680 "excluding a row this binding does not answer for is an ANSWER, not a blind spot"
2681 );
2682
2683 let mut unobserved = r.clone();
2685 unobserved.anchors.unobserved = 1;
2686 let roll = unobserved.rollup();
2687 assert_eq!(roll.verdict, RollupVerdict::Inconclusive);
2688 assert!(
2689 roll.blind_spots
2690 .iter()
2691 .any(|b| b.contains("could not be observed")),
2692 "and it names itself: {:?}",
2693 roll.blind_spots
2694 );
2695
2696 let mut span = r.clone();
2698 span.anchors.span_unvalidated = 2;
2699 assert_eq!(span.rollup().verdict, RollupVerdict::Inconclusive);
2700
2701 let mut ent = r.clone();
2703 ent.anchors.unreconciled = Some("the mem's lazy entity load has not run".into());
2704 assert_eq!(ent.rollup().verdict, RollupVerdict::Inconclusive);
2705 }
2706
2707 #[test]
2712 fn all_five_conditions_are_reachable_in_the_report() {
2713 let mut r = clean_report();
2714 r.anchors.excluded_out_of_scope = 1;
2715 r.anchors.excluded_other_binding = 1;
2716 r.anchors.excluded_artifacts = vec![
2717 "src/a.rs (out-of-scope)".into(),
2718 "src/b.rs (other-binding)".into(),
2719 ];
2720 r.anchors.dangling = 1;
2721 r.anchors.dangling_rows = vec!["engine--gone → src/c.rs".into()];
2722 r.anchors.span_unvalidated = 1;
2723 r.anchors.hash_from_backfill = 1;
2724
2725 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
2726 for (needle, condition) in [
2727 ("outside this binding's declared scope", "scope-excluded"),
2728 ("written by another binding", "other-binding"),
2729 ("no longer holds", "dangling entity"),
2730 ("never checked against their artifact", "span not validated"),
2731 ("inferred by backfill", "baseline established by backfill"),
2732 ] {
2733 assert!(
2734 md.contains(needle),
2735 "{condition} is not reachable in the report; looked for {needle:?} in:\n{md}"
2736 );
2737 }
2738 }
2739
2740 #[test]
2742 fn clean_requires_a_substantive_pass_and_no_findings() {
2743 let mut r = clean_report();
2744 assert_eq!(r.rollup().verdict, RollupVerdict::Clean);
2745 assert!(r.rollup().blind_spots.is_empty());
2746 assert!(r.rollup().actions.is_empty());
2747
2748 r.findings_by_class.insert("drifted".to_string(), 2);
2749 let roll = r.rollup();
2750 assert_eq!(roll.verdict, RollupVerdict::Drifted);
2751 assert_eq!(roll.findings_total, 2);
2752 assert!(
2753 roll.actions[0].contains("moved since the entity was written"),
2754 "the top action is the concrete next step: {:?}",
2755 roll.actions
2756 );
2757 }
2758
2759 #[test]
2764 fn a_vacuous_zero_over_zero_is_inconclusive_not_clean() {
2765 let mut r = clean_report();
2766 r.coverage.denominator = DenominatorBasis::Enumerated { count: 0 };
2767 let roll = r.rollup();
2768 assert_eq!(
2769 roll.verdict,
2770 RollupVerdict::Inconclusive,
2771 "0/0 is not a clean bill of health"
2772 );
2773 assert!(
2774 roll.blind_spots.iter().any(|s| s.contains("vacuous")),
2775 "the blindness is named, not implied: {:?}",
2776 roll.blind_spots
2777 );
2778 }
2779
2780 #[test]
2784 fn a_non_enumerable_facet_blocks_green_even_in_a_mixed_binding() {
2785 let mut r = clean_report();
2786 r.capabilities.push(FacetCapability {
2787 facet: "site".to_string(),
2788 medium_type: "web".to_string(),
2789 enumerable: false,
2790 change_signal: true,
2794 base_version_retrievable: false,
2795 anchor_namespace: "url".to_string(),
2796 signal: "none".to_string(),
2797 });
2798 assert!(matches!(
2800 r.coverage.denominator,
2801 DenominatorBasis::Enumerated { count } if count > 0
2802 ));
2803 let roll = r.rollup();
2804 assert_eq!(
2805 roll.verdict,
2806 RollupVerdict::Inconclusive,
2807 "one enumerable facet must not launder a non-enumerable one: {roll:?}"
2808 );
2809 assert!(
2810 roll.blind_spots
2811 .iter()
2812 .any(|s| s.contains("not enumerable")),
2813 "{:?}",
2814 roll.blind_spots
2815 );
2816 }
2817
2818 #[test]
2824 fn a_resolved_signal_of_none_blocks_green_even_when_the_medium_could_signal() {
2825 let mut r = clean_report();
2826 r.capabilities[0].change_signal = true;
2829 r.capabilities[0].signal = "none".to_string();
2830 r.freshness[0].change_detectable = false;
2831 r.freshness[0].signal = "none".to_string();
2832 let roll = r.rollup();
2833 assert_eq!(
2834 roll.verdict,
2835 RollupVerdict::Inconclusive,
2836 "a change-blind binding is not a clean bill of health: {roll:?}"
2837 );
2838 assert!(
2839 roll.blind_spots
2840 .iter()
2841 .any(|s| s.contains("could not read that signal")),
2842 "the blind spot names the unreadable signal: {:?}",
2843 roll.blind_spots
2844 );
2845 }
2846
2847 #[test]
2851 fn a_facet_without_a_change_signal_blocks_green() {
2852 let mut r = clean_report();
2853 r.capabilities[0].change_signal = false;
2854 let roll = r.rollup();
2855 assert_eq!(roll.verdict, RollupVerdict::Inconclusive);
2856 assert!(
2857 roll.blind_spots
2858 .iter()
2859 .any(|s| s.contains("no change signal")),
2860 "{:?}",
2861 roll.blind_spots
2862 );
2863 }
2864
2865 #[test]
2868 fn a_non_enumerable_scope_blocks_green() {
2869 let mut r = clean_report();
2870 r.coverage.denominator = DenominatorBasis::NonEnumerable {
2871 reason: "web medium".to_string(),
2872 };
2873 assert_eq!(r.rollup().verdict, RollupVerdict::Inconclusive);
2874 }
2875
2876 #[test]
2878 fn zero_observed_anchors_blocks_green() {
2879 let mut r = clean_report();
2880 r.anchors.observed = 0;
2881 r.anchors.resolves = 0;
2882 assert_eq!(r.rollup().verdict, RollupVerdict::Inconclusive);
2883 }
2884
2885 #[test]
2889 fn adopt_with_only_uncovered_is_never_red() {
2890 let mut r = clean_report();
2891 r.adopt = true;
2892 r.findings_by_class.insert("uncovered".to_string(), 12);
2893 let roll = r.rollup();
2894 assert_eq!(
2895 roll.verdict,
2896 RollupVerdict::Inconclusive,
2897 "onboarding is neither drift nor a clean bill: {roll:?}"
2898 );
2899 assert!(
2900 roll.because.contains("backfill worklist"),
2901 "the reason states the onboarding framing: {}",
2902 roll.because
2903 );
2904
2905 r.findings_by_class.insert("drifted".to_string(), 1);
2908 assert_eq!(r.rollup().verdict, RollupVerdict::Drifted);
2909 }
2910
2911 #[test]
2914 fn findings_outrank_blind_spots() {
2915 let mut r = clean_report();
2916 r.capabilities[0].change_signal = false;
2917 r.findings_by_class.insert("wrong".to_string(), 1);
2918 let roll = r.rollup();
2919 assert_eq!(roll.verdict, RollupVerdict::Drifted);
2920 assert!(
2921 !roll.blind_spots.is_empty(),
2922 "the blindness is still reported alongside the verdict"
2923 );
2924 }
2925
2926 #[test]
2929 fn actions_are_severity_ordered_and_never_drop_a_class() {
2930 let mut r = clean_report();
2931 r.findings_by_class.insert("uncovered".to_string(), 3);
2932 r.findings_by_class.insert("wrong".to_string(), 1);
2933 r.findings_by_class
2934 .insert("some-future-class".to_string(), 2);
2935 let roll = r.rollup();
2936 assert!(
2937 roll.actions[0].contains("contradict their source"),
2938 "{roll:?}"
2939 );
2940 assert_eq!(roll.actions.len(), 3, "{roll:?}");
2941 assert!(
2942 roll.actions.iter().any(|a| a.contains("some-future-class")),
2943 "an unranked class still surfaces: {roll:?}"
2944 );
2945 }
2946
2947 #[test]
2949 fn verdict_wire_strings_are_stable() {
2950 assert_eq!(RollupVerdict::Clean.wire(), "clean");
2951 assert_eq!(RollupVerdict::Drifted.wire(), "drifted");
2952 assert_eq!(RollupVerdict::Inconclusive.wire(), "inconclusive");
2953 let json = serde_json::to_string(&RollupVerdict::Inconclusive).unwrap();
2954 assert_eq!(json, "\"inconclusive\"");
2955 }
2956
2957 fn a3_workspace(root: &std::path::Path, files: &[&str]) {
2961 let mem_dir = root.join("mem");
2962 std::fs::create_dir_all(mem_dir.join(".memstead")).unwrap();
2963 std::fs::write(
2964 mem_dir.join(".memstead").join("config.json"),
2965 r#"{"format":1,"schema":"default@1.0.0","version":"1.0.0"}"#,
2966 )
2967 .unwrap();
2968 std::fs::create_dir_all(root.join(".memstead")).unwrap();
2969 std::fs::write(
2970 root.join(".memstead").join("workspace.toml"),
2971 "format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
2972 )
2973 .unwrap();
2974 let mount = crate::workspace::Mount {
2975 mem: "engine".to_string(),
2976 schema: Some("default@1.0.0".parse().unwrap()),
2977 storage: crate::workspace::MountStorage::Folder {
2978 path: mem_dir.clone(),
2979 },
2980 capability: crate::workspace::MountCapability::Write,
2981 lifecycle: crate::workspace::MountLifecycle::Eager,
2982 cross_linkable: false,
2983 migration_target: None,
2984 };
2985 crate::workspace_store::WorkspaceStoreAdapter::save_state(
2986 &crate::FileWorkspaceStore::new(),
2987 root,
2988 &crate::workspace::Workspace {
2989 mounts: vec![mount],
2990 settings: crate::workspace::WorkspaceSettings::default(),
2991 },
2992 )
2993 .unwrap();
2994 let out = std::process::Command::new("git")
2995 .args(["init", "-q"])
2996 .current_dir(root)
2997 .output()
2998 .unwrap();
2999 assert!(out.status.success());
3000 for f in files {
3001 let p = root.join(f);
3002 std::fs::create_dir_all(p.parent().unwrap()).unwrap();
3003 std::fs::write(p, "fn x() {}\n").unwrap();
3004 }
3005 }
3006
3007 fn a3_binding(
3008 sources: &[(&str, &str)],
3009 deny: &[&str],
3010 batch_size: u32,
3011 ) -> crate::binding::Binding {
3012 crate::binding::Binding {
3013 version: crate::binding::BINDING_VERSION,
3014 intent: None,
3015 sources: sources
3016 .iter()
3017 .map(|(name, glob)| crate::pipeline::Source {
3018 name: name.to_string(),
3019 medium_type: crate::pipeline::MediumType::Codebase,
3020 pointer: String::new(),
3021 change_detection: Some("git".to_string()),
3022 scope: vec![crate::pipeline::PatternEntry {
3023 path: glob.to_string(),
3024 mode: crate::pipeline::PatternMode::Allow,
3025 }],
3026 engagement: None,
3027 preparation: None,
3028 })
3029 .collect(),
3030 reference_mems: Vec::new(),
3031 destination_mem: "engine".to_string(),
3032 deny_paths: deny.iter().map(|d| d.to_string()).collect(),
3033 coverage_semantics: None,
3034 rules: None,
3035 prune: None,
3036 operations: crate::binding::Operations {
3037 build: Some(crate::binding::BuildOperation {
3038 mode: crate::binding::BuildMode::Discovery,
3039 trigger: crate::pipeline::IngestTrigger::Loop,
3040 batch_size,
3041 post_actions: None,
3042 }),
3043 sync: None,
3044 verify: Some(crate::binding::VerifyOperation {
3045 trigger: crate::pipeline::IngestTrigger::Manual,
3046 batch_size,
3047 adjudication_cap: crate::binding::DEFAULT_ADJUDICATION_CAP,
3048 full_resync_every: 0,
3050 }),
3051 },
3052 }
3053 }
3054
3055 #[test]
3059 fn coverage_counts_artifacts_described_not_anchor_rows() {
3060 let tmp = tempfile::tempdir().unwrap();
3061 let root = tmp.path();
3062 a3_workspace(root, &["src/three.rs", "src/two.rs", "src/none.rs"]);
3063 let mem_dir = root.join("mem");
3064 for slug in ["one", "left", "right"] {
3065 std::fs::write(
3066 mem_dir.join(format!("{slug}.md")),
3067 "---\ntype: decision\n---\n\n# E\n\n## Decision\n\nBody.\n",
3068 )
3069 .unwrap();
3070 }
3071 let mk = |artifact: &str| crate::anchor::Anchor {
3072 artifact: artifact.to_string(),
3073 grain: crate::anchor::AnchorGrain::File,
3074 class: crate::anchor::AnchorProvenanceClass::Anchored,
3075 at_version: None,
3076 hash: Some("recorded".to_string()),
3077 hash_stability: crate::anchor::AnchorHashStability::Stable,
3078 derived_from: Vec::new(),
3079 binding: None,
3080 source: None,
3081 span_unvalidated: false,
3082 hash_source: None,
3083 last_observed: None,
3084 };
3085 let mut sidecar = crate::anchor::AnchorSidecar::default();
3086 sidecar.set(
3087 "engine--one",
3088 vec![mk("src/three.rs"), mk("src/three.rs"), mk("src/three.rs")],
3089 );
3090 sidecar.set("engine--left", vec![mk("src/two.rs")]);
3091 sidecar.set("engine--right", vec![mk("src/two.rs")]);
3092 std::fs::write(
3093 mem_dir.join(crate::anchor::ANCHOR_SIDECAR_PATH),
3094 sidecar.to_bytes(),
3095 )
3096 .unwrap();
3097 let b = a3_binding(&[("graph", "src/**/*.rs")], &[], 20);
3098 crate::pipeline_store::write_binding(root, "engine", "graph", &b).unwrap();
3099
3100 let engine = crate::Engine::from_workspace_root(root).unwrap();
3101 let resolved = crate::ingest::resolve::resolve_binding_run("engine/graph", &b).unwrap();
3102 let outcome =
3103 crate::ingest::findings::verify_binding(&engine, root, &b, &resolved).unwrap();
3104 let report = super::compute_fidelity_report(&engine, root, &b, &resolved, &outcome.key);
3105 assert_eq!(
3106 report.coverage.denominator,
3107 super::DenominatorBasis::Enumerated { count: 3 }
3108 );
3109 assert_eq!(
3110 report.coverage.covered_artifacts, 2,
3111 "{:?}",
3112 report.coverage
3113 );
3114 assert_eq!(
3115 report.coverage.describing_entities, 3,
3116 "{:?}",
3117 report.coverage
3118 );
3119 assert_eq!(report.coverage.uncovered, vec!["src/none.rs".to_string()]);
3120 let md = super::render_fidelity_report(&report, 8_000, &[]).markdown;
3121 assert!(
3122 md.contains("coverage unit: describing entities per artifact"),
3123 "{md}"
3124 );
3125 assert!(
3126 md.contains("3 describing entities over 2 covered artifact(s)"),
3127 "{md}"
3128 );
3129 }
3130}