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 excluded: usize,
151 pub tree_anchors: Vec<TreeFanout>,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
159pub struct AnchorComposition {
160 pub by_class: BTreeMap<String, usize>,
164 pub by_grain: BTreeMap<String, usize>,
166 pub authored: usize,
169 pub observed: usize,
171 pub resolves: usize,
173 pub drifted: usize,
175 pub recheck: usize,
177 pub orphaned: usize,
179 pub unobserved: usize,
182 pub counted_rows: usize,
188 pub distinct_artifacts: usize,
193 pub excluded_other_binding: usize,
196 pub excluded_out_of_scope: usize,
199 pub excluded_artifacts: Vec<String>,
203 pub counted_without_provenance: usize,
207 pub dangling: usize,
212 pub dangling_rows: Vec<String>,
216 pub unreconciled: Option<String>,
220 pub span_unvalidated: usize,
225 pub hash_from_backfill: usize,
230 pub aging: Vec<AgingAnchor>,
235}
236
237#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
239pub struct AgingAnchor {
240 pub entity: String,
241 pub artifact: String,
242 pub observed_at: String,
243 pub unobserved_for_days: u64,
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
249pub struct FacetCapability {
250 pub facet: String,
252 pub medium_type: String,
254 pub enumerable: bool,
256 pub change_signal: bool,
258 pub base_version_retrievable: bool,
260 pub anchor_namespace: String,
262 pub signal: String,
265}
266
267impl FacetCapability {
268 fn from_caps(
269 facet: String,
270 medium_type: String,
271 caps: MediumCapabilities,
272 strategy: ChangeStrategy,
273 ) -> Self {
274 FacetCapability {
275 facet,
276 medium_type,
277 enumerable: caps.enumerable,
278 change_signal: caps.change_signal,
279 base_version_retrievable: caps.base_version_retrievable
286 && strategy_retrieves_base(strategy),
287 anchor_namespace: caps.anchor_namespace.to_string(),
288 signal: signal_wire(strategy).to_string(),
289 }
290 }
291}
292
293#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
295pub struct FacetFreshness {
296 pub facet: String,
298 pub signal: String,
300 pub synced: Option<String>,
302 pub verified: Option<String>,
304 pub change_detectable: bool,
309}
310
311#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
313pub struct FidelityReport {
314 pub binding: String,
316 pub destination_mem: String,
318 pub adopt: bool,
325 pub coverage_semantics: CoverageSemantics,
329 pub coverage_semantics_declared: bool,
334 pub legacy_dialect_patterns: Vec<String>,
340 pub capabilities: Vec<FacetCapability>,
342 pub freshness: Vec<FacetFreshness>,
344 pub source_moved_past_synced: Option<bool>,
348 pub coverage: GrainCoverage,
350 pub anchors: AnchorComposition,
352 pub findings_by_class: BTreeMap<String, usize>,
354 pub backlog: usize,
356 pub superseded: Vec<String>,
359 pub disposed_excluded: usize,
362 pub disposed_excluded_rationales: Vec<(String, String)>,
368 pub degradations: Vec<String>,
370}
371
372#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
388#[serde(rename_all = "lowercase")]
389pub enum RollupVerdict {
390 Clean,
392 Drifted,
394 Inconclusive,
397}
398
399impl RollupVerdict {
400 pub fn wire(&self) -> &'static str {
402 match self {
403 RollupVerdict::Clean => "clean",
404 RollupVerdict::Drifted => "drifted",
405 RollupVerdict::Inconclusive => "inconclusive",
406 }
407 }
408}
409
410#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
413pub struct Rollup {
414 pub verdict: RollupVerdict,
416 pub findings_total: usize,
418 pub because: String,
421 pub blind_spots: Vec<String>,
426 pub actions: Vec<String>,
429}
430
431const CLASS_SEVERITY: [&str; 5] = [
436 "wrong",
437 "drifted",
438 "unresolvable-anchor",
439 "uncovered",
440 "queued-for-adjudication",
441];
442
443fn class_action(class: &str, n: usize, binding: &str) -> String {
445 match class {
446 "wrong" => format!(
447 "{n} entity/entities contradict their source — read them against the source and \
448 correct the entity (`memstead projection brief {binding}` lists them)"
449 ),
450 "drifted" => format!(
451 "{n} anchored artifact(s) moved since the entity was written — re-read the source \
452 and update the entity, then re-verify with `--advance` to move the baseline"
453 ),
454 "unresolvable-anchor" => format!(
455 "{n} anchor(s) no longer resolve to anything — repoint them at the artifact's new \
456 location or unset them (`memstead_update` `anchors_unset`)"
457 ),
458 "uncovered" => format!(
459 "{n} in-scope source artifact(s) carry no anchor — cover them via \
460 `memstead projection brief {binding} --sync`, or record a disposition for the \
461 ones deliberately excluded"
462 ),
463 "queued-for-adjudication" => format!(
464 "{n} finding(s) are queued and not yet adjudicated — run \
465 `memstead projection verify {binding} --full` to work the backlog down"
466 ),
467 other => format!("{n} `{other}` finding(s) recorded"),
468 }
469}
470
471impl FidelityReport {
472 fn action_count(&self, class: &str, findings: usize) -> usize {
494 if class == "uncovered" {
495 self.coverage.uncovered.len()
496 } else {
497 findings
498 }
499 }
500
501 pub fn rollup(&self) -> Rollup {
502 let findings_total: usize = self.findings_by_class.values().sum();
503
504 let mut blind_spots: Vec<String> = Vec::new();
505 match &self.coverage.denominator {
506 DenominatorBasis::NonEnumerable { reason } => blind_spots.push(format!(
507 "the source scope is not enumerable ({reason}) — coverage is reported over \
508 anchors only, so an uncovered artifact cannot be detected"
509 )),
510 DenominatorBasis::Enumerated { count: 0 } => blind_spots.push(
511 "the enumerated source scope is empty (0 artifacts) — every coverage figure \
512 below is vacuous, not clean"
513 .to_string(),
514 ),
515 DenominatorBasis::Partial { count, reason } => blind_spots.push(format!(
516 "the source enumeration is INCOMPLETE ({reason}) — {count} artifact(s) \
517 survived, but their share of the population is unknown, so no coverage \
518 percentage is reported below"
519 )),
520 DenominatorBasis::Enumerated { .. } => {}
521 }
522 if !self.legacy_dialect_patterns.is_empty() {
523 blind_spots.push(format!(
524 "scope pattern(s) are still written against the workspace root rather than the \
525 source pointer and select nothing under the pointer join, so whatever they \
526 were meant to cover is absent from the denominator: {}. Rewrite them relative \
527 to the source's pointer",
528 self.legacy_dialect_patterns.join(", ")
529 ));
530 }
531 if self.anchors.observed == 0 {
532 blind_spots.push(
533 "no anchor carried a resolution state this pass — nothing was adjudicated"
534 .to_string(),
535 );
536 }
537 if self.anchors.unobserved > 0 {
549 blind_spots.push(format!(
550 "{} counted anchor(s) could not be observed at all this pass, so their state is unknown rather than clean",
551 self.anchors.unobserved
552 ));
553 }
554 if self.anchors.span_unvalidated > 0 {
555 blind_spots.push(format!(
556 "{} counted span anchor(s) were never checked against their artifact, so the span they name is unverified even where the hash resolves",
557 self.anchors.span_unvalidated
558 ));
559 }
560 if let Some(why) = &self.anchors.unreconciled {
561 blind_spots.push(format!(
562 "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"
563 ));
564 }
565 let change_blind: std::collections::BTreeSet<&str> = self
575 .freshness
576 .iter()
577 .filter(|f| !f.change_detectable)
578 .map(|f| f.facet.as_str())
579 .collect();
580 for cap in &self.capabilities {
581 if !cap.change_signal {
582 blind_spots.push(format!(
583 "facet `{}` ({}) provides no change signal — drift on it cannot be \
584 observed at all",
585 cap.facet, cap.medium_type
586 ));
587 } else if change_blind.contains(cap.facet.as_str()) {
588 blind_spots.push(format!(
589 "facet `{}` ({}) declares change-detection `{}` but this pass could \
590 not read that signal — either the binding asked for none, or the \
591 checkout cannot deliver it (a `git` source with no `.git`: an \
592 archive, a container COPY, a vendored drop). Drift on it cannot \
593 be observed",
594 cap.facet, cap.medium_type, cap.signal
595 ));
596 }
597 if !cap.enumerable {
607 blind_spots.push(format!(
608 "facet `{}` ({}) is not enumerable — an uncovered artifact under it \
609 cannot be detected, only an anchored one",
610 cap.facet, cap.medium_type
611 ));
612 }
613 }
614
615 let mut actions: Vec<String> = Vec::new();
616 for class in CLASS_SEVERITY {
617 if let Some(&n) = self.findings_by_class.get(class)
618 && n > 0
619 {
620 actions.push(class_action(
621 class,
622 self.action_count(class, n),
623 &self.binding,
624 ));
625 }
626 }
627 for (class, &n) in &self.findings_by_class {
630 if n > 0 && !CLASS_SEVERITY.contains(&class.as_str()) {
631 actions.push(class_action(class, n, &self.binding));
632 }
633 }
634
635 let only_uncovered = findings_total > 0
641 && self
642 .findings_by_class
643 .iter()
644 .all(|(class, &n)| n == 0 || class == "uncovered");
645
646 let (verdict, because) = if self.adopt && only_uncovered {
647 (
648 RollupVerdict::Inconclusive,
649 format!(
650 "this mem predates its binding — the {findings_total} uncovered artifact(s) \
651 are the backfill worklist, not drift"
652 ),
653 )
654 } else if findings_total > 0 {
655 let tally = self
656 .findings_by_class
657 .iter()
658 .filter(|(_, n)| **n > 0)
659 .map(|(class, n)| format!("{class}: {n}"))
660 .collect::<Vec<_>>()
661 .join(", ");
662 (
663 RollupVerdict::Drifted,
664 format!("{findings_total} finding(s) recorded over the current key ({tally})"),
665 )
666 } else if !blind_spots.is_empty() {
667 (
668 RollupVerdict::Inconclusive,
669 format!(
670 "no findings recorded, but the pass could not speak to {} axis/axes — \
671 this is not a clean bill of health",
672 blind_spots.len()
673 ),
674 )
675 } else {
676 (
677 RollupVerdict::Clean,
678 "the pass was substantive on every axis and recorded no findings".to_string(),
679 )
680 };
681
682 Rollup {
683 verdict,
684 findings_total,
685 because,
686 blind_spots,
687 actions,
688 }
689 }
690}
691
692#[derive(Debug, Clone, PartialEq, Eq)]
699pub struct RenderedFidelityReport {
700 pub markdown: String,
702 pub mode: String,
705 pub hints: Vec<(String, usize)>,
708 pub budget_used: usize,
710}
711
712fn ratio(num: usize, den: usize) -> String {
718 if den == 0 {
719 format!("{num}/{den} (n/a)")
720 } else {
721 let pct = (num as f64) * 100.0 / (den as f64);
722 format!("{num}/{den} ({pct:.1}%)")
723 }
724}
725
726fn render_hard_required(report: &FidelityReport) -> String {
730 let mut md = String::new();
731 md.push_str(&format!("# Fidelity report — `{}`\n\n", report.binding));
732
733 let rollup = report.rollup();
738 md.push_str(&format!(
739 "**Verdict: {}** — {}.\n\n",
740 rollup.verdict.wire().to_uppercase(),
741 rollup.because
742 ));
743 if !rollup.actions.is_empty() {
744 md.push_str("**Do next:**\n\n");
745 for action in &rollup.actions {
746 md.push_str(&format!("1. {action}\n"));
747 }
748 md.push('\n');
749 }
750 if !rollup.blind_spots.is_empty() {
751 md.push_str("**This pass could not see:**\n\n");
752 for spot in &rollup.blind_spots {
753 md.push_str(&format!("- {spot}\n"));
754 }
755 md.push('\n');
756 }
757
758 md.push_str(&format!(
759 "- **Destination mem:** `{}`\n- **Coverage semantics:** {}{}\n\n",
760 report.destination_mem,
761 match report.coverage_semantics {
762 CoverageSemantics::Exhaustive => "exhaustive",
763 CoverageSemantics::Curated => "curated",
764 },
765 if report.coverage_semantics_declared {
766 ""
767 } else {
768 " (resolved from the sources' media — not declared)"
769 }
770 ));
771
772 if report.adopt {
779 md.push_str("## Adopting — first verify\n\n");
780 md.push_str(
781 "This mem predates its binding: it carries no anchors and has no prior sync \
782 baseline, so **0% anchored is expected — this is onboarding, not a failure.** \
783 Do not read the coverage numbers below as drift or a red verdict; the uncovered \
784 artifacts are the backfill worklist, not defects.\n\n",
785 );
786 md.push_str(&format!(
787 "**Backfill path:** run `memstead projection brief {} --sync` to work through the in-scope \
788 source artifacts that carry no entity yet, covering the clearly-new concepts among \
789 them through the normal mutation surface. Backfilling is incremental — a partial \
790 pass is fine, and the next sync continues where you left off.\n\n",
791 report.binding
792 ));
793 }
794
795 md.push_str("## Denominator provenance\n\n");
797 match &report.coverage.denominator {
798 DenominatorBasis::Enumerated { count } => md.push_str(&format!(
799 "Coverage is reported relative to the per-medium enumeration `S(D)` = **{count}** \
800 source artifact(s) in scope (after `deny_paths`).\n\n"
801 )),
802 DenominatorBasis::NonEnumerable { reason } => md.push_str(&format!(
803 "No `S(D)` denominator: {reason}. Coverage is reported over anchors only; the \
804 per-medium enumeration is unavailable.\n\n"
805 )),
806 DenominatorBasis::Partial { count, reason } => md.push_str(&format!(
807 "`S(D)` is **partial**: {reason}. **{count}** source artifact(s) were \
808 enumerated by the patterns that did resolve, but that set is not the \
809 population, so the coverage figures below are counts and carry no \
810 percentage.\n\n"
811 )),
812 }
813
814 md.push_str("## Capability matrix\n\n");
816 if report.capabilities.is_empty() {
817 md.push_str("_(no primary sources resolved)_\n\n");
818 } else {
819 for c in &report.capabilities {
820 md.push_str(&format!("### `{}` ({})\n\n", c.facet, c.medium_type));
821 md.push_str(&format!(
822 "- enumerable: {} | change_signal: {} | base_version_retrievable: {}\n",
823 c.enumerable, c.change_signal, c.base_version_retrievable
824 ));
825 md.push_str(&format!(
826 "- anchor_namespace: `{}` | resolved signal: `{}`\n\n",
827 c.anchor_namespace, c.signal
828 ));
829 }
830 }
831
832 md.push_str("## Freshness\n\n");
834 if report.freshness.is_empty() {
835 md.push_str("_(no source facets)_\n\n");
836 } else {
837 for f in &report.freshness {
838 md.push_str(&format!("### `{}`\n\n", f.facet));
839 md.push_str(&format!("- signal: `{}`\n", f.signal));
840 if !f.change_detectable {
841 md.push_str(
845 "- **freshness unknowable** — this medium is not change-detectable \
846 (no change signal); `#synced` / `#verified` cannot be adjudicated as fresh\n",
847 );
848 } else {
849 match &f.synced {
850 Some(t) => md.push_str(&format!("- `#synced`: `{t}`\n")),
851 None => md.push_str("- `#synced`: never synced\n"),
852 }
853 match &f.verified {
854 Some(t) => md.push_str(&format!("- `#verified`: `{t}`\n")),
855 None => md.push_str("- `#verified`: never verified\n"),
856 }
857 }
858 md.push('\n');
859 }
860 match report.source_moved_past_synced {
862 Some(true) => md.push_str(
863 "**Source moved past its `#synced` baseline** — the graph is stale for the \
864 moved facet(s); a sync pass is due.\n\n",
865 ),
866 Some(false) => {
867 md.push_str("Every change-detectable source is at its `#synced` baseline.\n\n")
868 }
869 None => {}
870 }
871 }
872
873 md.push_str("## Coverage (grain-classed)\n\n");
875 let den = match &report.coverage.denominator {
879 DenominatorBasis::Enumerated { count } => *count,
880 DenominatorBasis::NonEnumerable { .. } | DenominatorBasis::Partial { .. } => 0,
881 };
882 md.push_str(&format!(
883 "- direct-covered (file / span anchors): {}\n",
884 ratio(report.coverage.direct_covered, den)
885 ));
886 let tree_files: usize = report.coverage.tree_anchors.iter().map(|t| t.fanout).sum();
889 md.push_str(&format!(
890 "- tree-anchor fan-out (separate axis): {} tree anchor(s) fanning out over {} file(s); \
891 {} file(s) covered ONLY via a tree anchor\n",
892 report.coverage.tree_anchors.len(),
893 tree_files,
894 report.coverage.tree_only_covered
895 ));
896 md.push_str(&format!(
897 "- uncovered (no anchor): {}{}\n",
898 report.coverage.uncovered.len(),
899 if report.coverage.excluded > 0 {
900 format!(
901 "; excluded on purpose (not owed): {}",
902 report.coverage.excluded
903 )
904 } else {
905 String::new()
906 }
907 ));
908 md.push_str(&format!(
911 "- coverage unit: {} — {} describing entit{} over {} covered artifact(s)\n\n",
912 report.coverage.unit,
913 report.coverage.describing_entities,
914 if report.coverage.describing_entities == 1 {
915 "y"
916 } else {
917 "ies"
918 },
919 report.coverage.covered_artifacts
920 ));
921
922 match report.coverage_semantics {
927 CoverageSemantics::Exhaustive if report.adopt => {
928 let backlog = report.coverage.uncovered.len();
931 md.push_str(&format!(
932 "**Exhaustive coverage (onboarding):** {backlog} in-scope artifact(s) carry no \
933 entity yet ({} disposed excluded{}) — the expected first-sync backfill \
934 worklist for a mem that predates its binding, not defects.\n\n",
935 report.disposed_excluded,
936 already_out_clause(report.disposed_excluded)
937 ));
938 }
939 CoverageSemantics::Exhaustive => {
940 let findings = report.coverage.uncovered.len();
942 md.push_str(&format!(
943 "**Exhaustive coverage:** {findings} unaccounted artifact(s) — not anchored, not \
944 declared-excluded, no persisted disposition ({} disposed excluded{}) — are \
945 **findings**.\n\n",
946 report.disposed_excluded,
947 already_out_clause(report.disposed_excluded)
948 ));
949 }
950 CoverageSemantics::Curated => {
951 md.push_str(&format!(
952 "**Curated coverage:** {} unaccounted artifact(s) are **information**, not \
953 defects — a curated binding covers a deliberate slice.\n\n",
954 report.coverage.uncovered.len()
955 ));
956 }
957 }
958
959 if !report.disposed_excluded_rationales.is_empty() {
963 md.push_str("**Excluded on purpose (persisted dispositions):**\n");
964 for (artifact, rationale) in &report.disposed_excluded_rationales {
965 if rationale.is_empty() {
966 md.push_str(&format!("- `{artifact}`\n"));
967 } else {
968 md.push_str(&format!("- `{artifact}` — {rationale}\n"));
969 }
970 }
971 md.push('\n');
972 }
973
974 md.push_str("## Anchors\n\n");
976 md.push_str(&format!(
977 "- by class: {}\n",
978 render_counts(&report.anchors.by_class)
979 ));
980 md.push_str(&format!(
981 "- by grain: {}\n",
982 render_counts(&report.anchors.by_grain)
983 ));
984 md.push_str(&format!(
985 "- `authored` bucket (excluded from coverage/accuracy denominators): {}\n",
986 report.anchors.authored
987 ));
988 md.push_str(&format!(
995 "- resolution (non-`authored`, observed): resolves {}, drifted {}, recheck {}, \
996 orphaned {}; **anchor-resolution %:** {} over {} counted row(s) on {} distinct \
997 artifact(s), with {} unobserved this pass (state unavailable, never scored as \
998 resolved)\n",
999 report.anchors.resolves,
1000 report.anchors.drifted,
1001 report.anchors.recheck,
1002 report.anchors.orphaned,
1003 ratio(report.anchors.resolves, report.anchors.observed),
1004 report.anchors.counted_rows,
1005 report.anchors.distinct_artifacts,
1006 report.anchors.unobserved
1007 ));
1008 md.push_str(&format!(
1014 "- the figures above count anchor ROWS: {} row(s) over {} distinct artifact(s)\n",
1015 report.anchors.counted_rows, report.anchors.distinct_artifacts
1016 ));
1017 if !report.anchors.aging.is_empty() {
1021 md.push_str(&format!(
1022 "- {} counted row(s) rest on a recorded observation rather than a live one \
1023 (url anchors; the engine never fetches):\n",
1024 report.anchors.aging.len()
1025 ));
1026 const AGING_CAP: usize = 10;
1027 for a in report.anchors.aging.iter().take(AGING_CAP) {
1028 md.push_str(&format!(
1029 " - `{}` → `{}`: unobserved for {} days (observed {})\n",
1030 a.entity, a.artifact, a.unobserved_for_days, a.observed_at
1031 ));
1032 }
1033 if report.anchors.aging.len() > AGING_CAP {
1034 md.push_str(&format!(
1035 " - …and {} more\n",
1036 report.anchors.aging.len() - AGING_CAP
1037 ));
1038 }
1039 }
1040 if report.anchors.excluded_other_binding > 0 || report.anchors.excluded_out_of_scope > 0 {
1043 md.push_str(&format!(
1044 "- excluded from this binding's population: {} written by another binding, \
1045 {} outside this binding's declared scope (legal, reported here, never deleted)\n",
1046 report.anchors.excluded_other_binding, report.anchors.excluded_out_of_scope
1047 ));
1048 const NAMED_CAP: usize = 10;
1055 for a in report.anchors.excluded_artifacts.iter().take(NAMED_CAP) {
1056 md.push_str(&format!(" - {a}\n"));
1057 }
1058 if report.anchors.excluded_artifacts.len() > NAMED_CAP {
1059 md.push_str(&format!(
1060 " - …and {} more (counts above are complete)\n",
1061 report.anchors.excluded_artifacts.len() - NAMED_CAP
1062 ));
1063 }
1064 }
1065 match (&report.anchors.unreconciled, report.anchors.dangling) {
1070 (Some(why), _) => md.push_str(&format!(
1071 "- the entity end of these anchors was NOT reconciled this pass ({why}), so \
1072 dangling sidecar rows would not have been detected\n"
1073 )),
1074 (None, 0) => {}
1075 (None, n) => {
1076 md.push_str(&format!(
1077 "- {n} sidecar row(s) name an entity this mem no longer holds. Excluded from \
1078 every figure above, reported rather than repaired: the row is the trace of a \
1079 writer that went around the engine\n"
1080 ));
1081 const NAMED_CAP: usize = 10;
1082 for r in report.anchors.dangling_rows.iter().take(NAMED_CAP) {
1083 md.push_str(&format!(" - {r}\n"));
1084 }
1085 if report.anchors.dangling_rows.len() > NAMED_CAP {
1086 md.push_str(&format!(
1087 " - …and {} more (the count above is complete)\n",
1088 report.anchors.dangling_rows.len() - NAMED_CAP
1089 ));
1090 }
1091 }
1092 }
1093 if report.anchors.span_unvalidated > 0 {
1099 md.push_str(&format!(
1100 "- {} counted span row(s) were never checked against their artifact, so their \
1101 span is unverified even where the hash resolves\n",
1102 report.anchors.span_unvalidated
1103 ));
1104 }
1105 if report.anchors.hash_from_backfill > 0 {
1106 md.push_str(&format!(
1107 "- {} counted row(s) carry a baseline the engine inferred by backfill rather than \
1108 one an author pinned\n",
1109 report.anchors.hash_from_backfill
1110 ));
1111 }
1112 if report.anchors.counted_without_provenance > 0 {
1113 md.push_str(&format!(
1114 "- {} counted anchor(s) record no producing binding and are included by the \
1115 pre-provenance fallback, so this population rests partly on that fallback \
1116 rather than wholly on provenance\n",
1117 report.anchors.counted_without_provenance
1118 ));
1119 }
1120 md.push('\n');
1121
1122 md.push_str("## Findings\n\n");
1124 md.push_str(&format!(
1125 "- by class: {}\n",
1126 render_counts(&report.findings_by_class)
1127 ));
1128 md.push_str(&format!(
1129 "- **tier-3 adjudication backlog:** {}\n",
1130 report.backlog
1131 ));
1132 md.push_str(&format!(
1133 "- superseded (prior `hash(D)`, segregated): {}\n\n",
1134 report.superseded.len()
1135 ));
1136
1137 md.push_str("## Degradations\n\n");
1139 if report.degradations.is_empty() {
1140 md.push_str("_(none)_\n\n");
1141 } else {
1142 for d in &report.degradations {
1143 md.push_str(&format!("- {d}\n"));
1144 }
1145 md.push('\n');
1146 }
1147
1148 md
1149}
1150
1151fn render_counts(counts: &BTreeMap<String, usize>) -> String {
1153 if counts.is_empty() {
1154 return "(none)".to_string();
1155 }
1156 counts
1157 .iter()
1158 .map(|(k, v)| format!("{k}={v}"))
1159 .collect::<Vec<_>>()
1160 .join(", ")
1161}
1162
1163fn heavy_sections(report: &FidelityReport) -> Vec<(&'static str, String)> {
1167 let mut out: Vec<(&'static str, String)> = Vec::new();
1168
1169 let mut s = String::new();
1171 if !report.coverage.uncovered.is_empty() {
1172 s.push_str("## Uncovered artifacts\n\n");
1173 for a in &report.coverage.uncovered {
1174 s.push_str(&format!("- `{a}`\n"));
1175 }
1176 s.push('\n');
1177 }
1178 out.push(("uncovered_artifacts", s));
1179
1180 let mut s = String::new();
1182 if !report.coverage.tree_anchors.is_empty() {
1183 s.push_str("## Tree-anchor fan-out (detail)\n\n");
1184 for t in &report.coverage.tree_anchors {
1185 s.push_str(&format!(
1186 "- `{}` → `{}` fans out over {} file(s)\n",
1187 t.entity, t.artifact, t.fanout
1188 ));
1189 }
1190 s.push('\n');
1191 }
1192 out.push(("tree_fanout", s));
1193
1194 let mut s = String::new();
1196 if !report.superseded.is_empty() {
1197 s.push_str("## Superseded findings (detail)\n\n");
1198 for f in &report.superseded {
1199 s.push_str(&format!("- {f}\n"));
1200 }
1201 s.push('\n');
1202 }
1203 out.push(("superseded_findings", s));
1204
1205 out
1206}
1207
1208fn already_out_clause(disposed_excluded: usize) -> &'static str {
1214 if disposed_excluded > 0 {
1215 ", already out of that count"
1216 } else {
1217 ""
1218 }
1219}
1220
1221pub fn render_fidelity_report(
1232 report: &FidelityReport,
1233 budget: usize,
1234 include: &[String],
1235) -> RenderedFidelityReport {
1236 let hard = render_hard_required(report);
1237 let hard_cost = estimate_tokens(&hard);
1238 let overbudget = hard_cost > budget;
1239
1240 let include_set: std::collections::BTreeSet<&str> = include
1241 .iter()
1242 .map(String::as_str)
1243 .filter(|k| ALLOWED_REPORT_INCLUDE_KEYS.contains(k))
1244 .collect();
1245 let unknown_includes: Vec<&String> = include
1246 .iter()
1247 .filter(|k| !ALLOWED_REPORT_INCLUDE_KEYS.contains(&k.as_str()))
1248 .collect();
1249
1250 let sections = heavy_sections(report);
1251 let mut emitted: Vec<String> = Vec::new();
1252 let mut hints: Vec<(String, usize)> = Vec::new();
1253 let mut used = hard_cost;
1254 let mut remaining = budget.saturating_sub(hard_cost);
1255
1256 for (key, section_md) in §ions {
1257 if section_md.is_empty() {
1258 continue; }
1260 let cost = estimate_tokens(section_md);
1261 let forced = include_set.contains(key);
1262 if forced {
1263 emitted.push(section_md.clone());
1264 used += cost;
1265 remaining = remaining.saturating_sub(cost);
1266 } else if !overbudget && remaining >= cost {
1267 emitted.push(section_md.clone());
1268 used += cost;
1269 remaining -= cost;
1270 } else {
1271 hints.push(((*key).to_string(), cost));
1272 }
1273 }
1274
1275 let mode = if overbudget {
1276 "overbudget"
1277 } else if hints.is_empty() {
1278 "complete"
1279 } else {
1280 "reduced"
1281 };
1282
1283 let mut md = String::new();
1284 md.push_str("---\n");
1285 md.push_str(&format!("_report_mode: {mode}\n"));
1286 md.push_str(&format!("_budget_requested: {budget}\n"));
1287 md.push_str(&format!("_budget_used: {used}\n"));
1288 md.push_str("---\n\n");
1289 md.push_str(&hard);
1290 for section in &emitted {
1291 md.push_str(section);
1292 }
1293
1294 if !hints.is_empty() {
1295 md.push_str("## Hints\n\n");
1296 md.push_str(
1297 "_(heavy sections omitted under the token budget — re-query with the key)_\n\n",
1298 );
1299 for (key, tokens) in &hints {
1300 md.push_str(&format!("- `{key}` — estimated_tokens: {tokens}\n"));
1301 }
1302 md.push('\n');
1303 }
1304
1305 if !unknown_includes.is_empty() {
1306 md.push_str("## Warnings\n\n");
1307 for k in &unknown_includes {
1308 md.push_str(&format!(
1309 "- unknown include key `{k}` — allowed: {}\n",
1310 ALLOWED_REPORT_INCLUDE_KEYS.join(", ")
1311 ));
1312 }
1313 md.push('\n');
1314 }
1315
1316 RenderedFidelityReport {
1317 markdown: md,
1318 mode: mode.to_string(),
1319 hints,
1320 budget_used: used,
1321 }
1322}
1323
1324pub fn compute_fidelity_report(
1339 engine: &Engine,
1340 workspace_root: &Path,
1341 binding: &Binding,
1342 resolved: &ResolvedIngest,
1343 key: &FindingKey,
1344) -> FidelityReport {
1345 let binding_id = resolved.name.clone();
1346 let dest = resolved.destination_mem.clone();
1347
1348 let sync_state = engine
1350 .mem_config_for(&dest)
1351 .map(|c| c.sync_state.clone())
1352 .unwrap_or_default();
1353 let mut capabilities: Vec<FacetCapability> = Vec::new();
1354 let mut freshness: Vec<FacetFreshness> = Vec::new();
1355 let mut any_change_detectable = false;
1356 for source in &resolved.sources {
1357 let ResolvedSource::Primary(p) = source else {
1358 continue;
1359 };
1360 let caps = medium_capabilities(p.medium_type);
1361 let medium_type = serde_json::to_value(p.medium_type)
1362 .ok()
1363 .and_then(|v| v.as_str().map(str::to_string))
1364 .unwrap_or_default();
1365 let strategy = resolve_change_strategy(p, workspace_root);
1366 let signal = signal_wire(strategy).to_string();
1367 let signal_readable = match strategy {
1377 ChangeStrategy::Git => {
1378 super::resolve::find_git_root(&super::resolve::source_base_path(p, workspace_root))
1379 .is_some()
1380 }
1381 _ => true,
1382 };
1383 let change_detectable =
1384 caps.change_signal && strategy != ChangeStrategy::None && signal_readable;
1385 any_change_detectable |= change_detectable;
1386
1387 capabilities.push(FacetCapability::from_caps(
1388 p.name.clone(),
1389 medium_type,
1390 caps,
1391 strategy,
1392 ));
1393
1394 let synced = sync_state
1395 .get(&format!("{binding_id}/{}#synced", p.name))
1396 .cloned();
1397 let verified = sync_state
1398 .get(&format!("{binding_id}/{}#verified", p.name))
1399 .cloned();
1400 freshness.push(FacetFreshness {
1401 facet: p.name.clone(),
1402 signal,
1403 synced,
1404 verified,
1405 change_detectable,
1406 });
1407 }
1408
1409 let source_moved_past_synced = if any_change_detectable {
1410 Some(source_moved(engine, resolved, workspace_root))
1411 } else {
1412 None
1413 };
1414
1415 let mut s_d: Vec<String> = Vec::new();
1417 let mut enumerable_facets = 0usize;
1418 let mut empty_enumerable_facets: BTreeSet<String> = BTreeSet::new();
1425 let mut malformed_patterns: Vec<String> = Vec::new();
1430 let mut legacy_patterns: Vec<String> = Vec::new();
1431 let mut partiality_reasons: Vec<String> = Vec::new();
1437 for source in &resolved.sources {
1438 if let ResolvedSource::Primary(p) = source {
1439 let caps = medium_capabilities(p.medium_type);
1440 if caps.enumerable {
1441 enumerable_facets += 1;
1442 }
1443 let walked = enumerate_source_artifacts_reported(
1444 engine,
1445 p,
1446 &resolved.deny_paths,
1447 workspace_root,
1448 );
1449 if caps.enumerable && walked.files.is_empty() {
1450 empty_enumerable_facets.insert(p.name.clone());
1451 }
1452 for m in &walked.malformed {
1453 malformed_patterns.push(format!("`{}` in facet `{}`", m, p.name));
1454 }
1455 for note in &walked.legacy_dialect {
1456 legacy_patterns.push(format!("`{}` in facet `{}`", note.pattern, p.name));
1457 }
1458 if let Some(reason) = walked.partiality_reason() {
1459 partiality_reasons.push(format!("facet `{}`: {reason}", p.name));
1460 }
1461 s_d.extend(walked.files);
1462 }
1463 }
1464 s_d.sort();
1465 s_d.dedup();
1466
1467 let denominator = if !partiality_reasons.is_empty() {
1468 DenominatorBasis::Partial {
1471 count: s_d.len(),
1472 reason: partiality_reasons.join("; "),
1473 }
1474 } else if !s_d.is_empty() {
1475 DenominatorBasis::Enumerated { count: s_d.len() }
1476 } else if enumerable_facets == 0 {
1477 DenominatorBasis::NonEnumerable {
1478 reason: "the medium type(s) are not enumerable this cycle".to_string(),
1479 }
1480 } else if !legacy_patterns.is_empty() {
1481 DenominatorBasis::NonEnumerable {
1486 reason: format!(
1487 "scope pattern(s) still written against the workspace root rather than the \
1488 source pointer, so they select nothing under the pointer join: {}. Rewrite \
1489 them relative to the source's pointer",
1490 legacy_patterns.join(", ")
1491 ),
1492 }
1493 } else {
1494 DenominatorBasis::NonEnumerable {
1498 reason: "no source artifacts enumerated in scope".to_string(),
1499 }
1500 };
1501
1502 let mut direct_covered = 0usize;
1503 let mut tree_only_covered = 0usize;
1504 let mut uncovered: Vec<String> = Vec::new();
1505 let mut describing: BTreeSet<String> = BTreeSet::new();
1506 let mut tree_fanout: BTreeMap<(String, String), usize> = BTreeMap::new();
1507 let entity_end_reconciled = engine.entity_set_is_reconcilable(dest.as_str()).is_ok();
1508 for file in &s_d {
1509 let refs = engine.anchors_referencing_artifact(file);
1523 let mine: Vec<&(crate::EntityId, crate::anchor::Anchor)> = refs
1524 .iter()
1525 .filter(|(eid, a)| {
1526 eid.mem() == dest.as_str()
1527 && a.binding
1528 .as_deref()
1529 .map(|b| b == key.binding_hash.as_str())
1530 .unwrap_or(true)
1531 && (!entity_end_reconciled || !engine.entity_is_absent(eid))
1532 })
1533 .collect();
1534 if mine.is_empty() {
1535 uncovered.push(file.clone());
1536 continue;
1537 }
1538 describing.extend(mine.iter().map(|(eid, _)| eid.as_ref().to_string()));
1539 let has_non_tree = mine.iter().any(|(_, a)| a.grain != AnchorGrain::Tree);
1540 if has_non_tree {
1541 direct_covered += 1;
1542 } else {
1543 tree_only_covered += 1;
1544 }
1545 for (eid, a) in &mine {
1547 if a.grain == AnchorGrain::Tree {
1548 *tree_fanout
1549 .entry((eid.as_ref().to_string(), a.artifact.clone()))
1550 .or_insert(0) += 1;
1551 }
1552 }
1553 }
1554 let tree_anchors: Vec<TreeFanout> = tree_fanout
1555 .into_iter()
1556 .map(|((entity, artifact), fanout)| TreeFanout {
1557 entity,
1558 artifact,
1559 fanout,
1560 })
1561 .collect();
1562
1563 let mut disposed_excluded_rationales: Vec<(String, String)> = Vec::new();
1573 if let Some((mem, name)) = binding_id.split_once('/')
1574 && let Ok(Some(state)) = read_advance_store(workspace_root, mem, name)
1575 {
1576 let uncovered_set: std::collections::BTreeSet<&str> =
1577 uncovered.iter().map(String::as_str).collect();
1578 for (artifact, rationale) in &state.exclusions {
1579 if uncovered_set.contains(artifact.as_str()) {
1580 disposed_excluded_rationales.push((artifact.clone(), rationale.clone()));
1581 }
1582 }
1583 }
1584 let disposed_excluded = disposed_excluded_rationales.len();
1585 let excluded_set: std::collections::BTreeSet<&str> = disposed_excluded_rationales
1586 .iter()
1587 .map(|(a, _)| a.as_str())
1588 .collect();
1589 uncovered.retain(|f| !excluded_set.contains(f.as_str()));
1590
1591 let coverage = GrainCoverage {
1592 denominator,
1593 covered_artifacts: direct_covered + tree_only_covered,
1594 describing_entities: describing.len(),
1595 unit: COVERAGE_UNIT,
1596 direct_covered,
1597 tree_only_covered,
1598 uncovered: uncovered.clone(),
1599 excluded: disposed_excluded,
1600 tree_anchors,
1601 };
1602
1603 let population = crate::ingest::anchor_population::population_for(
1607 engine,
1608 resolved,
1609 Some(key.binding_hash.as_str()),
1610 );
1611 let mut anchors = AnchorComposition {
1612 counted_rows: population.included.len(),
1613 distinct_artifacts: population.distinct_artifacts(),
1614 excluded_other_binding: population
1615 .excluded_count(crate::ingest::anchor_population::ExclusionReason::OtherBinding),
1616 excluded_out_of_scope: population
1617 .excluded_count(crate::ingest::anchor_population::ExclusionReason::OutOfScope),
1618 excluded_artifacts: population
1619 .excluded
1620 .iter()
1621 .map(|e| format!("{} ({})", e.artifact, e.reason.as_wire()))
1622 .collect(),
1623 counted_without_provenance: population.without_provenance,
1624 dangling: population.dangling.len(),
1625 dangling_rows: population
1626 .dangling
1627 .iter()
1628 .map(|d| format!("{} → {}", d.entity, d.artifact))
1629 .collect(),
1630 unreconciled: population.unreconciled.map(str::to_string),
1631 span_unvalidated: population
1632 .included
1633 .iter()
1634 .filter(|(_, r)| r.anchor.span_unvalidated)
1635 .count(),
1636 hash_from_backfill: population
1637 .included
1638 .iter()
1639 .filter(|(_, r)| {
1640 r.anchor.hash_source == Some(crate::anchor::AnchorHashSource::Backfill)
1641 })
1642 .count(),
1643 aging: {
1644 let today = crate::engine::mutation::iso_now();
1645 let mut rows: Vec<AgingAnchor> = population
1646 .included
1647 .iter()
1648 .filter_map(|(eid, r)| {
1649 let at = r.observed_at.as_deref()?;
1650 Some(AgingAnchor {
1651 entity: eid.as_ref().to_string(),
1652 artifact: r.anchor.artifact.clone(),
1653 observed_at: at.to_string(),
1654 unobserved_for_days: crate::anchor::days_between(at, &today).unwrap_or(0),
1655 })
1656 })
1657 .collect();
1658 rows.sort_by_key(|a| std::cmp::Reverse(a.unobserved_for_days));
1659 rows
1660 },
1661 ..Default::default()
1662 };
1663 for (_eid, resolved_anchor) in population.included {
1664 let a = &resolved_anchor.anchor;
1665 *anchors
1666 .by_class
1667 .entry(a.class.as_wire().to_string())
1668 .or_insert(0) += 1;
1669 *anchors
1670 .by_grain
1671 .entry(a.grain.as_wire().to_string())
1672 .or_insert(0) += 1;
1673 if a.class == AnchorProvenanceClass::Authored {
1674 anchors.authored += 1;
1675 continue; }
1677 match resolved_anchor.state {
1678 Some(AnchorState::Resolves) => {
1679 anchors.resolves += 1;
1680 anchors.observed += 1;
1681 }
1682 Some(AnchorState::Drifted) => {
1683 anchors.drifted += 1;
1684 anchors.observed += 1;
1685 }
1686 Some(AnchorState::Recheck) => {
1687 anchors.recheck += 1;
1688 anchors.observed += 1;
1689 }
1690 Some(AnchorState::Orphaned) => {
1691 anchors.orphaned += 1;
1692 anchors.observed += 1;
1693 }
1694 None => anchors.unobserved += 1,
1695 }
1696 }
1697
1698 let mut findings_by_class: BTreeMap<String, usize> = BTreeMap::new();
1700 let mut backlog = 0usize;
1701 let mut superseded: Vec<String> = Vec::new();
1702 if let Some((mem, name)) = binding_id.split_once('/')
1703 && let Ok(Some(store)) = read_findings_store(workspace_root, mem, name)
1704 {
1705 for f in store.current(key) {
1706 *findings_by_class
1707 .entry(f.class.as_wire().to_string())
1708 .or_insert(0) += 1;
1709 if f.class == FindingClass::QueuedForAdjudication {
1710 backlog += 1;
1711 }
1712 }
1713 for f in store.superseded(key) {
1714 superseded.push(format!(
1715 "[{}] {} ({})",
1716 f.class.as_wire(),
1717 finding_target_label(&f.target),
1718 f.facet
1719 ));
1720 }
1721 }
1722
1723 let mut degradations: Vec<String> = Vec::new();
1725 for c in &capabilities {
1726 if !c.change_signal || c.signal == "none" {
1727 degradations.push(format!(
1728 "change-signal-none:`{}` — freshness is unknowable for this facet",
1729 c.facet
1730 ));
1731 }
1732 if !c.enumerable {
1733 degradations.push(format!(
1734 "enumeration-unavailable:`{}` — `S(D)` coverage denominator not computable",
1735 c.facet
1736 ));
1737 } else if empty_enumerable_facets.contains(&c.facet) {
1738 degradations.push(format!(
1746 "enumeration-empty:`{}` — the medium claims enumerability but the walk yielded \
1747 no artifacts; coverage is reported over anchors only",
1748 c.facet
1749 ));
1750 }
1751 if !c.base_version_retrievable {
1752 degradations.push(format!(
1753 "base-version-unretrievable:`{}` — prune degrades to conflict-flagging",
1754 c.facet
1755 ));
1756 }
1757 }
1758 if anchors.recheck > 0 {
1759 degradations.push(format!(
1760 "hash-adjudication-deferred — {} anchor(s) recheck (unstable medium / hash \
1761 unavailable), not asserted drift",
1762 anchors.recheck
1763 ));
1764 }
1765 if anchors.unobserved > 0 {
1766 degradations.push(format!(
1767 "anchors-unobserved — {} anchor(s) could not be observed this pass",
1768 anchors.unobserved
1769 ));
1770 }
1771
1772 let adopt = super::render::mem_predates_binding(engine, resolved);
1776 let effective_coverage = crate::binding::effective_coverage_semantics(binding);
1777
1778 FidelityReport {
1779 legacy_dialect_patterns: legacy_patterns,
1780 binding: binding_id,
1781 destination_mem: dest,
1782 adopt,
1783 coverage_semantics: effective_coverage.value,
1784 coverage_semantics_declared: effective_coverage.declared,
1785 capabilities,
1786 freshness,
1787 source_moved_past_synced,
1788 coverage,
1789 anchors,
1790 findings_by_class,
1791 backlog,
1792 superseded,
1793 disposed_excluded,
1794 disposed_excluded_rationales,
1795 degradations,
1796 }
1797}
1798
1799fn strategy_retrieves_base(strategy: ChangeStrategy) -> bool {
1808 matches!(strategy, ChangeStrategy::Git | ChangeStrategy::Graph)
1809}
1810
1811fn signal_wire(strategy: ChangeStrategy) -> &'static str {
1814 match strategy {
1815 ChangeStrategy::None => "none",
1816 ChangeStrategy::Git => "git",
1817 ChangeStrategy::Mtime => "mtime",
1818 ChangeStrategy::Graph => "graph",
1819 }
1820}
1821
1822fn finding_target_label(target: &super::findings::FindingTarget) -> String {
1824 match target {
1825 super::findings::FindingTarget::Anchor { entity, artifact } => {
1826 format!("{entity} → {artifact}")
1827 }
1828 super::findings::FindingTarget::Artifact { artifact } => artifact.clone(),
1829 }
1830}
1831
1832#[cfg(test)]
1833mod tests {
1834 use super::*;
1835
1836 fn base_report() -> FidelityReport {
1839 FidelityReport {
1840 legacy_dialect_patterns: Vec::new(),
1841 binding: "engine/graph".to_string(),
1842 destination_mem: "engine".to_string(),
1843 adopt: false,
1844 coverage_semantics: CoverageSemantics::Exhaustive,
1845 coverage_semantics_declared: true,
1846 capabilities: vec![FacetCapability {
1847 facet: "src".to_string(),
1848 medium_type: "codebase".to_string(),
1849 enumerable: true,
1850 change_signal: true,
1851 base_version_retrievable: true,
1852 anchor_namespace: "path".to_string(),
1853 signal: "git".to_string(),
1854 }],
1855 freshness: vec![FacetFreshness {
1856 facet: "src".to_string(),
1857 signal: "git".to_string(),
1858 synced: Some("deadbeef".to_string()),
1859 verified: None,
1860 change_detectable: true,
1861 }],
1862 source_moved_past_synced: Some(false),
1863 coverage: GrainCoverage {
1864 denominator: DenominatorBasis::Enumerated { count: 10 },
1865 covered_artifacts: 9,
1866 describing_entities: 4,
1867 unit: COVERAGE_UNIT,
1868 direct_covered: 6,
1869 tree_only_covered: 3,
1870 uncovered: vec!["src/a.rs".to_string()],
1871 excluded: 0,
1872 tree_anchors: vec![TreeFanout {
1873 entity: "engine--big".to_string(),
1874 artifact: "src/".to_string(),
1875 fanout: 3,
1876 }],
1877 },
1878 anchors: AnchorComposition {
1879 by_class: BTreeMap::from([
1880 ("anchored".to_string(), 5),
1881 ("authored".to_string(), 2),
1882 ]),
1883 by_grain: BTreeMap::from([("file".to_string(), 4), ("tree".to_string(), 1)]),
1884 authored: 2,
1885 observed: 5,
1886 resolves: 4,
1887 drifted: 0,
1888 recheck: 1,
1889 orphaned: 0,
1890 unobserved: 0,
1891 ..Default::default()
1892 },
1893 findings_by_class: BTreeMap::from([
1894 ("uncovered".to_string(), 1),
1895 ("queued-for-adjudication".to_string(), 1),
1896 ]),
1897 backlog: 1,
1898 superseded: Vec::new(),
1899 disposed_excluded: 0,
1900 disposed_excluded_rationales: Vec::new(),
1901 degradations: vec!["hash-adjudication-deferred — 1 anchor(s) recheck".to_string()],
1902 }
1903 }
1904
1905 #[test]
1908 fn aging_rows_render_with_their_age() {
1909 let mut r = base_report();
1910 r.anchors.aging = vec![AgingAnchor {
1911 entity: "engine--cites".to_string(),
1912 artifact: "https://w.test/living".to_string(),
1913 observed_at: "2026-08-03T09:00:00Z".to_string(),
1914 unobserved_for_days: 30,
1915 }];
1916 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
1917 assert!(
1918 md.contains("1 counted row(s) rest on a recorded observation"),
1919 "{md}"
1920 );
1921 assert!(
1922 md.contains(
1923 "`engine--cites` → `https://w.test/living`: unobserved for 30 days (observed 2026-08-03T09:00:00Z)"
1924 ),
1925 "{md}"
1926 );
1927 let plain = render_fidelity_report(&base_report(), 8_000, &[]).markdown;
1928 assert!(!plain.contains("recorded observation"), "{plain}");
1929 }
1930
1931 #[test]
1936 fn b1_renders_all_elements_deterministically() {
1937 let r = base_report();
1938 let a = render_fidelity_report(&r, 8_000, &[]);
1939 let b = render_fidelity_report(&r, 8_000, &[]);
1940 assert_eq!(a.markdown, b.markdown, "deterministic — identical bytes");
1941
1942 let md = &a.markdown;
1943 assert!(md.contains("direct-covered (file / span anchors): 6/10"));
1945 assert!(md.contains(
1946 "tree-anchor fan-out (separate axis): 1 tree anchor(s) fanning out over 3 file(s)"
1947 ));
1948 assert!(
1950 !md.contains("9/10"),
1951 "tree fan-out must not blend into direct coverage"
1952 );
1953 assert!(md.contains("anchor-resolution %:** 4/5"));
1955 assert!(md.contains("`authored` bucket (excluded from coverage/accuracy denominators): 2"));
1957 assert!(md.contains("tier-3 adjudication backlog:** 1"));
1959 assert!(md.contains("## Capability matrix"));
1961 assert!(md.contains("## Degradations"));
1962 assert!(md.contains("hash-adjudication-deferred"));
1963 assert!(md.contains("per-medium enumeration `S(D)` = **10**"));
1965 }
1966
1967 #[test]
1970 fn b2_detectionless_medium_freshness_unknowable_never_green() {
1971 let mut r = base_report();
1972 r.capabilities = vec![FacetCapability {
1973 facet: "manual".to_string(),
1974 medium_type: "web".to_string(),
1975 enumerable: false,
1976 change_signal: false,
1977 base_version_retrievable: false,
1978 anchor_namespace: "url".to_string(),
1979 signal: "none".to_string(),
1980 }];
1981 r.freshness = vec![FacetFreshness {
1982 facet: "manual".to_string(),
1983 signal: "none".to_string(),
1984 synced: Some("should-never-render-green".to_string()),
1987 verified: Some("nor-this".to_string()),
1988 change_detectable: false,
1989 }];
1990 r.source_moved_past_synced = None;
1991 let out = render_fidelity_report(&r, 8_000, &[]);
1992 let md = &out.markdown;
1993 assert!(md.contains("signal: `none`"));
1994 assert!(md.contains("freshness unknowable"));
1995 assert!(!md.contains("should-never-render-green"));
1998 assert!(
1999 !md.contains("`#synced`: `"),
2000 "no synced token rendered for a non-detectable medium"
2001 );
2002 assert!(
2003 !md.contains("at its `#synced` baseline"),
2004 "no green 'at baseline' verdict"
2005 );
2006 }
2007
2008 #[test]
2016 fn b1_base_retrievability_follows_resolved_strategy_not_medium_ceiling() {
2017 use crate::pipeline::MediumType;
2018
2019 assert!(medium_capabilities(MediumType::Filesystem).base_version_retrievable);
2021
2022 let fs_mtime = FacetCapability::from_caps(
2024 "prose".to_string(),
2025 "filesystem".to_string(),
2026 medium_capabilities(MediumType::Filesystem),
2027 ChangeStrategy::Mtime,
2028 );
2029 assert!(
2030 !fs_mtime.base_version_retrievable,
2031 "filesystem+mtime has no retrievable base leg — degrades to conflict-flag"
2032 );
2033 assert_eq!(fs_mtime.signal, "mtime");
2034
2035 let fs_git = FacetCapability::from_caps(
2036 "prose".to_string(),
2037 "filesystem".to_string(),
2038 medium_capabilities(MediumType::Filesystem),
2039 ChangeStrategy::Git,
2040 );
2041 assert!(
2042 fs_git.base_version_retrievable,
2043 "filesystem backed by git keeps the never-clobber base leg"
2044 );
2045
2046 assert!(!strategy_retrieves_base(ChangeStrategy::None));
2048 assert!(!strategy_retrieves_base(ChangeStrategy::Mtime));
2049 assert!(strategy_retrieves_base(ChangeStrategy::Git));
2050 assert!(strategy_retrieves_base(ChangeStrategy::Graph));
2051
2052 let mut r = base_report();
2056 r.capabilities = vec![fs_mtime.clone()];
2057 r.degradations = if !fs_mtime.base_version_retrievable {
2058 vec![format!(
2059 "base-version-unretrievable:`{}` — prune degrades to conflict-flagging",
2060 fs_mtime.facet
2061 )]
2062 } else {
2063 Vec::new()
2064 };
2065 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
2066 assert!(
2067 md.contains("base-version-unretrievable:`prose` — prune degrades to conflict-flagging"),
2068 "filesystem+mtime surfaces the conflict-flag degradation in the report"
2069 );
2070 }
2071
2072 #[test]
2075 fn b3_aggregates_always_ship_at_zero_budget() {
2076 let r = base_report();
2077 let out = render_fidelity_report(&r, 0, &[]);
2078 assert_eq!(out.mode, "overbudget");
2079 let md = &out.markdown;
2080 assert!(md.contains("direct-covered (file / span anchors): 6/10"));
2082 assert!(md.contains("tier-3 adjudication backlog:** 1"));
2083 assert!(md.contains("## Capability matrix"));
2084 assert!(!md.contains("## Uncovered artifacts"));
2086 assert!(md.contains("## Hints"));
2087 assert!(out.hints.iter().any(|(k, _)| k == "uncovered_artifacts"));
2088 }
2089
2090 #[test]
2094 fn b3_large_facet_list_truncates_then_include_forces() {
2095 let mut r = base_report();
2096 r.coverage.uncovered = (0..500).map(|i| format!("src/file_{i}.rs")).collect();
2098 let hard_cost = estimate_tokens(&render_hard_required(&r));
2100 let out = render_fidelity_report(&r, hard_cost + 5, &[]);
2101 assert_eq!(out.mode, "reduced");
2102 assert!(
2103 !out.markdown.contains("src/file_499.rs"),
2104 "big list not rendered unbounded"
2105 );
2106 assert!(out.markdown.contains("## Hints"));
2107 let (_, est) = out
2108 .hints
2109 .iter()
2110 .find(|(k, _)| k == "uncovered_artifacts")
2111 .expect("uncovered list hinted");
2112 assert!(*est > 5, "the hint carries a real estimated_tokens figure");
2113
2114 let forced =
2116 render_fidelity_report(&r, hard_cost + 5, &["uncovered_artifacts".to_string()]);
2117 assert!(
2118 forced.markdown.contains("src/file_499.rs"),
2119 "include forces the full list"
2120 );
2121 }
2122
2123 #[test]
2126 fn b4_curated_vs_exhaustive_framing() {
2127 let mut exhaustive = base_report();
2128 exhaustive.coverage_semantics = CoverageSemantics::Exhaustive;
2129 let ex_md = render_fidelity_report(&exhaustive, 8_000, &[]).markdown;
2130 assert!(ex_md.contains("Exhaustive coverage:"));
2131 assert!(ex_md.contains("are **findings**"));
2132
2133 let mut curated = base_report();
2134 curated.coverage_semantics = CoverageSemantics::Curated;
2135 let cur_md = render_fidelity_report(&curated, 8_000, &[]).markdown;
2136 assert!(cur_md.contains("Curated coverage:"));
2137 assert!(cur_md.contains("**information**"));
2138 assert!(
2139 !cur_md.contains("are **findings**"),
2140 "curated never frames unaccounted as findings"
2141 );
2142 }
2143
2144 #[test]
2155 fn b4_disposition_excludes_from_exhaustive_findings() {
2156 let mut r = base_report();
2157 r.coverage_semantics = CoverageSemantics::Exhaustive;
2158 r.coverage.uncovered = vec!["src/a.rs".to_string()];
2160 r.coverage.excluded = 1;
2161 r.disposed_excluded = 1;
2162 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
2163 assert!(md.contains("1 unaccounted artifact(s)"), "{md}");
2164 assert!(
2165 md.contains("1 disposed excluded, already out of that count"),
2166 "{md}"
2167 );
2168 assert!(
2173 md.contains("- uncovered (no anchor): 1; excluded on purpose (not owed): 1"),
2174 "{md}"
2175 );
2176 }
2177
2178 #[test]
2188 fn c9_the_uncovered_action_counts_the_body_list_not_the_recorded_findings() {
2189 let mut r = base_report();
2190 r.coverage_semantics = CoverageSemantics::Exhaustive;
2191 r.coverage.uncovered = vec![
2194 "src/a.rs".to_string(),
2195 "src/b.rs".to_string(),
2196 "src/c.rs".to_string(),
2197 ];
2198 r.findings_by_class = [("uncovered".to_string(), 1)].into_iter().collect();
2199 let rollup = r.rollup();
2200 let uncovered_action = rollup
2201 .actions
2202 .iter()
2203 .find(|a| a.contains("carry no anchor"))
2204 .expect("the uncovered action is present");
2205 assert!(
2206 uncovered_action.starts_with("3 in-scope source artifact(s)"),
2207 "the sentence counts the enumerated list it describes: {uncovered_action}"
2208 );
2209 assert_eq!(rollup.findings_total, 1, "{rollup:?}");
2212
2213 let mut d = base_report();
2215 d.coverage.uncovered = vec!["src/a.rs".to_string(), "src/b.rs".to_string()];
2216 d.findings_by_class = [("drifted".to_string(), 1)].into_iter().collect();
2217 let drift_action = d
2218 .rollup()
2219 .actions
2220 .iter()
2221 .find(|a| a.contains("moved since the entity was written"))
2222 .cloned()
2223 .expect("the drift action is present");
2224 assert!(
2225 drift_action.starts_with("1 anchored artifact(s)"),
2226 "{drift_action}"
2227 );
2228 }
2229
2230 #[test]
2244 fn c7_no_exclusions_renders_byte_identically_to_the_pre_plan_output() {
2245 let mut r = base_report();
2246 r.coverage_semantics = CoverageSemantics::Exhaustive;
2247 r.coverage.uncovered = vec!["src/a.rs".to_string(), "src/b.rs".to_string()];
2248 r.coverage.excluded = 0;
2249 r.disposed_excluded = 0;
2250 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
2251 assert!(md.contains("- uncovered (no anchor): 2\n"), "{md}");
2252 assert!(!md.contains("excluded on purpose"), "{md}");
2253 assert!(
2255 md.contains("2 unaccounted artifact(s)") && md.contains("(0 disposed excluded)"),
2256 "{md}"
2257 );
2258 assert!(!md.contains("already out of that count"), "{md}");
2259
2260 let mut adopt = r.clone();
2262 adopt.adopt = true;
2263 let md = render_fidelity_report(&adopt, 8_000, &[]).markdown;
2264 assert!(md.contains("(0 disposed excluded)"), "{md}");
2265 assert!(!md.contains("already out of that count"), "{md}");
2266 }
2267
2268 #[test]
2271 fn b4_authored_exclusion_rationale_is_rendered() {
2272 let mut r = base_report();
2273 r.coverage_semantics = CoverageSemantics::Exhaustive;
2274 r.coverage.uncovered = vec!["src/gen.rs".to_string()];
2275 r.disposed_excluded = 1;
2276 r.disposed_excluded_rationales =
2277 vec![("src/gen.rs".to_string(), "generated; no entity".to_string())];
2278 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
2279 assert!(md.contains("Excluded on purpose (persisted dispositions):"));
2280 assert!(md.contains("`src/gen.rs` — generated; no entity"));
2281 }
2282
2283 #[test]
2286 fn b5_denominator_provenance_stated() {
2287 let r = base_report();
2288 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
2289 assert!(md.contains("## Denominator provenance"));
2290 assert!(md.contains("per-medium enumeration `S(D)` = **10**"));
2291
2292 let mut non = base_report();
2293 non.coverage.denominator = DenominatorBasis::NonEnumerable {
2294 reason: "the medium type(s) are not enumerable this cycle".to_string(),
2295 };
2296 let md2 = render_fidelity_report(&non, 8_000, &[]).markdown;
2297 assert!(md2.contains("No `S(D)` denominator"));
2298 assert!(md2.contains("not enumerable this cycle"));
2299 }
2300
2301 #[test]
2307 fn e1_adopt_report_renders_onboarding_no_red_verdict() {
2308 let mut r = base_report();
2309 r.adopt = true;
2310 r.coverage_semantics = CoverageSemantics::Exhaustive;
2311 r.coverage.uncovered = (0..5).map(|i| format!("src/file_{i}.rs")).collect();
2312 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
2313
2314 assert!(md.contains("## Adopting — first verify"));
2316 assert!(md.contains("0% anchored is expected — this is onboarding, not a failure."));
2317 assert!(
2319 md.contains("**Backfill path:** run `memstead projection brief engine/graph --sync`")
2320 );
2321 assert!(
2324 !md.contains("are **findings**"),
2325 "pre-binding history must not produce a red findings verdict"
2326 );
2327 assert!(md.contains("Exhaustive coverage (onboarding):"));
2328 assert!(md.contains("backfill worklist"));
2329
2330 r.adopt = false;
2332 let md2 = render_fidelity_report(&r, 8_000, &[]).markdown;
2333 assert!(!md2.contains("## Adopting — first verify"));
2334 assert!(md2.contains("are **findings**"));
2335 }
2336
2337 #[test]
2339 fn unknown_include_key_warns() {
2340 let r = base_report();
2341 let out = render_fidelity_report(&r, 8_000, &["bogus".to_string()]);
2342 assert!(out.markdown.contains("unknown include key `bogus`"));
2343 }
2344
2345 use crate::anchor::{Anchor, AnchorHashStability, AnchorProvenanceClass, AnchorSidecar};
2348 use crate::binding::{
2349 BINDING_VERSION, Binding, BuildMode, BuildOperation, DEFAULT_ADJUDICATION_CAP,
2350 DEFAULT_FULL_RESYNC_EVERY, Operations, VerifyOperation,
2351 };
2352 use crate::ingest::findings::verify_binding;
2353 use crate::ingest::resolve::resolve_binding_run;
2354 use crate::pipeline::{IngestTrigger, MediumType, PatternEntry, PatternMode};
2355 use crate::pipeline_store::{load_pipeline_configs, write_binding};
2356 use crate::workspace::{
2357 Mount, MountCapability, MountLifecycle, MountStorage, Workspace, WorkspaceSettings,
2358 };
2359 use crate::workspace_store::WorkspaceStoreAdapter;
2360
2361 #[test]
2368 fn compute_report_end_to_end() {
2369 let tmp = tempfile::tempdir().unwrap();
2370 let (report, outcome, md) = end_to_end_report(tmp.path(), &["direct", "tree", "auth"]);
2371 end_to_end_body(&report, &outcome, &md);
2372 }
2373
2374 #[test]
2379 fn coverage_does_not_rest_on_an_anchor_whose_entity_is_gone() {
2380 let tmp = tempfile::tempdir().unwrap();
2381 let (report, _outcome, md) = end_to_end_report(tmp.path(), &["tree"]);
2382 assert_eq!(
2383 report.coverage.direct_covered, 0,
2384 "the only direct anchor on present.rs is dangling, so nothing covers it directly"
2385 );
2386 assert!(
2387 report
2388 .coverage
2389 .uncovered
2390 .contains(&"src/present.rs".to_string()),
2391 "and the artifact reads uncovered rather than covered by a phantom"
2392 );
2393 assert_eq!(report.anchors.dangling, 2);
2395 assert_eq!(report.anchors.counted_rows, 1);
2396 assert_eq!(report.anchors.unreconciled, None);
2397 assert!(
2398 md.contains("name an entity this mem no longer holds"),
2399 "and the report says so on the page, not only in the struct"
2400 );
2401 }
2402
2403 fn end_to_end_report(
2409 root: &std::path::Path,
2410 entity_slugs: &[&str],
2411 ) -> (
2412 FidelityReport,
2413 crate::ingest::findings::VerifyOutcome,
2414 String,
2415 ) {
2416 let mem_dir = root.join("mem");
2417 std::fs::create_dir_all(mem_dir.join(".memstead")).unwrap();
2418 std::fs::write(
2419 mem_dir.join(".memstead").join("config.json"),
2420 r#"{"format":1,"schema":"default@1.0.0","version":"1.0.0"}"#,
2421 )
2422 .unwrap();
2423
2424 std::fs::create_dir_all(root.join(".memstead")).unwrap();
2425 std::fs::write(
2426 root.join(".memstead").join("workspace.toml"),
2427 "format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
2428 )
2429 .unwrap();
2430 let mount = Mount {
2431 mem: "engine".to_string(),
2432 schema: Some("default@1.0.0".parse().unwrap()),
2433 storage: MountStorage::Folder {
2434 path: mem_dir.clone(),
2435 },
2436 capability: MountCapability::Write,
2437 lifecycle: MountLifecycle::Eager,
2438 cross_linkable: false,
2439 migration_target: None,
2440 };
2441 crate::FileWorkspaceStore::new()
2442 .save_state(
2443 root,
2444 &Workspace {
2445 mounts: vec![mount],
2446 settings: WorkspaceSettings::default(),
2447 },
2448 )
2449 .unwrap();
2450
2451 let out = std::process::Command::new("git")
2452 .args(["init", "-q"])
2453 .current_dir(root)
2454 .output()
2455 .unwrap();
2456 assert!(out.status.success());
2457 std::fs::create_dir_all(root.join("src").join("sub")).unwrap();
2458 std::fs::write(root.join("src").join("present.rs"), "fn a() {}\n").unwrap();
2459 std::fs::write(root.join("src").join("uncovered.rs"), "fn b() {}\n").unwrap();
2460 std::fs::write(root.join("src").join("sub").join("deep.rs"), "fn c() {}\n").unwrap();
2461
2462 let mk = |artifact: &str, grain: AnchorGrain, class: AnchorProvenanceClass| Anchor {
2463 artifact: artifact.to_string(),
2464 grain,
2465 class,
2466 at_version: None,
2467 hash: class.is_hash_bearing().then(|| "recorded".to_string()),
2468 hash_stability: AnchorHashStability::Stable,
2469 derived_from: Vec::new(),
2470 binding: None,
2471 source: None,
2472 span_unvalidated: false,
2473 hash_source: None,
2474 last_observed: None,
2475 };
2476 for slug in entity_slugs {
2480 std::fs::write(
2481 mem_dir.join(format!("{slug}.md")),
2482 "---\ntype: decision\n---\n\n# E\n\n## Decision\n\nBody.\n",
2483 )
2484 .unwrap();
2485 }
2486 let mut sidecar = AnchorSidecar::default();
2487 sidecar.set(
2488 "engine--direct",
2489 vec![mk(
2490 "src/present.rs",
2491 AnchorGrain::File,
2492 AnchorProvenanceClass::Anchored,
2493 )],
2494 );
2495 sidecar.set(
2496 "engine--tree",
2497 vec![mk(
2498 "src/sub/",
2499 AnchorGrain::Tree,
2500 AnchorProvenanceClass::Anchored,
2501 )],
2502 );
2503 sidecar.set(
2505 "engine--auth",
2506 vec![mk(
2507 "src/present.rs",
2508 AnchorGrain::File,
2509 AnchorProvenanceClass::Authored,
2510 )],
2511 );
2512 std::fs::write(
2513 mem_dir.join(crate::anchor::ANCHOR_SIDECAR_PATH),
2514 sidecar.to_bytes(),
2515 )
2516 .unwrap();
2517
2518 write_binding(
2519 root,
2520 "engine",
2521 "graph",
2522 &Binding {
2523 version: BINDING_VERSION,
2524 intent: None,
2525 sources: vec![crate::pipeline::Source {
2526 name: "graph".to_string(),
2527 medium_type: MediumType::Codebase,
2528 pointer: String::new(),
2529 change_detection: Some("git".to_string()),
2530 scope: vec![PatternEntry {
2531 path: "src/**/*.rs".to_string(),
2532 mode: PatternMode::Allow,
2533 }],
2534 engagement: None,
2535 preparation: None,
2536 }],
2537 reference_mems: Vec::new(),
2538 destination_mem: "engine".to_string(),
2539 deny_paths: Vec::new(),
2540 coverage_semantics: None,
2541 rules: None,
2542 prune: None,
2543 operations: Operations {
2544 build: Some(BuildOperation {
2545 mode: BuildMode::Discovery,
2546 trigger: IngestTrigger::Loop,
2547 batch_size: 20,
2548 post_actions: None,
2549 }),
2550 sync: None,
2551 verify: Some(VerifyOperation {
2552 trigger: IngestTrigger::Manual,
2553 batch_size: 20,
2554 adjudication_cap: DEFAULT_ADJUDICATION_CAP,
2555 full_resync_every: DEFAULT_FULL_RESYNC_EVERY,
2556 }),
2557 },
2558 },
2559 )
2560 .unwrap();
2561
2562 let engine = Engine::from_workspace_root(root).unwrap();
2563 let configs = load_pipeline_configs(root).unwrap();
2564 let binding = &configs.bindings[0].config;
2565 let resolved = resolve_binding_run("engine/graph", binding).unwrap();
2566
2567 let outcome = verify_binding(&engine, root, binding, &resolved).unwrap();
2569
2570 let report = compute_fidelity_report(&engine, root, binding, &resolved, &outcome.key);
2572 let md = render_fidelity_report(&report, 8_000, &[]).markdown;
2573 (report, outcome, md)
2574 }
2575
2576 fn end_to_end_body(
2577 report: &FidelityReport,
2578 outcome: &crate::ingest::findings::VerifyOutcome,
2579 md: &str,
2580 ) {
2581 assert_eq!(
2583 report.coverage.denominator,
2584 DenominatorBasis::Enumerated { count: 3 }
2585 );
2586 assert_eq!(report.coverage.direct_covered, 1);
2589 assert_eq!(report.coverage.tree_only_covered, 1);
2590 assert_eq!(
2591 report.coverage.uncovered,
2592 vec!["src/uncovered.rs".to_string()]
2593 );
2594 assert_eq!(report.coverage.tree_anchors.len(), 1);
2596 assert_eq!(report.coverage.tree_anchors[0].fanout, 1);
2597 assert_eq!(report.coverage.tree_anchors[0].artifact, "src/sub/");
2598 assert_eq!(report.anchors.authored, 1);
2600 assert_eq!(report.anchors.by_class.get("authored"), Some(&1));
2601 assert_eq!(report.anchors.observed, 2);
2606 assert_eq!(report.anchors.recheck, 1);
2607 assert_eq!(report.anchors.drifted, 1);
2608 assert_eq!(report.backlog, outcome.backlog);
2610 assert!(
2612 report
2613 .degradations
2614 .iter()
2615 .any(|d| d.contains("hash-adjudication-deferred"))
2616 );
2617 assert!(md.contains("per-medium enumeration `S(D)` = **3**"));
2619 assert!(!report.adopt);
2622 assert!(!md.contains("## Adopting — first verify"));
2623 }
2624
2625 #[test]
2630 fn compute_report_adopt_when_mem_predates_binding() {
2631 let tmp = tempfile::tempdir().unwrap();
2632 let root = tmp.path();
2633 let mem_dir = root.join("mem");
2634 std::fs::create_dir_all(mem_dir.join(".memstead")).unwrap();
2635 std::fs::write(
2636 mem_dir.join(".memstead").join("config.json"),
2637 r#"{"format":1,"schema":"default@1.0.0","version":"1.0.0"}"#,
2638 )
2639 .unwrap();
2640 std::fs::create_dir_all(root.join(".memstead")).unwrap();
2641 std::fs::write(
2642 root.join(".memstead").join("workspace.toml"),
2643 "format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
2644 )
2645 .unwrap();
2646 let mount = Mount {
2647 mem: "engine".to_string(),
2648 schema: Some("default@1.0.0".parse().unwrap()),
2649 storage: MountStorage::Folder {
2650 path: mem_dir.clone(),
2651 },
2652 capability: MountCapability::Write,
2653 lifecycle: MountLifecycle::Eager,
2654 cross_linkable: false,
2655 migration_target: None,
2656 };
2657 crate::FileWorkspaceStore::new()
2658 .save_state(
2659 root,
2660 &Workspace {
2661 mounts: vec![mount],
2662 settings: WorkspaceSettings::default(),
2663 },
2664 )
2665 .unwrap();
2666 let out = std::process::Command::new("git")
2667 .args(["init", "-q"])
2668 .current_dir(root)
2669 .output()
2670 .unwrap();
2671 assert!(out.status.success());
2672 std::fs::create_dir_all(root.join("src")).unwrap();
2673 std::fs::write(root.join("src").join("a.rs"), "fn a() {}\n").unwrap();
2675 std::fs::write(root.join("src").join("b.rs"), "fn b() {}\n").unwrap();
2676
2677 write_binding(
2678 root,
2679 "engine",
2680 "graph",
2681 &Binding {
2682 version: BINDING_VERSION,
2683 intent: None,
2684 sources: vec![crate::pipeline::Source {
2685 name: "graph".to_string(),
2686 medium_type: MediumType::Codebase,
2687 pointer: String::new(),
2688 change_detection: Some("git".to_string()),
2689 scope: vec![PatternEntry {
2690 path: "src/**/*.rs".to_string(),
2691 mode: PatternMode::Allow,
2692 }],
2693 engagement: None,
2694 preparation: None,
2695 }],
2696 reference_mems: Vec::new(),
2697 destination_mem: "engine".to_string(),
2698 deny_paths: Vec::new(),
2699 coverage_semantics: None,
2700 rules: None,
2701 prune: None,
2702 operations: Operations {
2703 build: Some(BuildOperation {
2704 mode: BuildMode::Discovery,
2705 trigger: IngestTrigger::Loop,
2706 batch_size: 20,
2707 post_actions: None,
2708 }),
2709 sync: None,
2710 verify: Some(VerifyOperation {
2711 trigger: IngestTrigger::Manual,
2712 batch_size: 20,
2713 adjudication_cap: DEFAULT_ADJUDICATION_CAP,
2714 full_resync_every: DEFAULT_FULL_RESYNC_EVERY,
2715 }),
2716 },
2717 },
2718 )
2719 .unwrap();
2720
2721 let engine = Engine::from_workspace_root(root).unwrap();
2722 let configs = load_pipeline_configs(root).unwrap();
2723 let binding = &configs.bindings[0].config;
2724 let resolved = resolve_binding_run("engine/graph", binding).unwrap();
2725 let outcome = verify_binding(&engine, root, binding, &resolved).unwrap();
2726 let report = compute_fidelity_report(&engine, root, binding, &resolved, &outcome.key);
2727
2728 assert!(
2730 report.adopt,
2731 "a no-anchor, never-synced mem predates its binding"
2732 );
2733 let md = render_fidelity_report(&report, 8_000, &[]).markdown;
2734 assert!(md.contains("## Adopting — first verify"));
2735 assert!(md.contains("0% anchored is expected"));
2736 assert!(!md.contains("are **findings**"));
2738 assert!(md.contains("Exhaustive coverage (onboarding):"));
2739 }
2740
2741 #[test]
2745 fn report_marks_resolved_coverage_semantics() {
2746 let mut resolved = base_report();
2747 resolved.coverage_semantics = CoverageSemantics::Curated;
2748 resolved.coverage_semantics_declared = false;
2749 let md = render_hard_required(&resolved);
2750 assert!(
2751 md.contains("curated (resolved from the sources' media — not declared)"),
2752 "resolved value carries the marker: {md}"
2753 );
2754
2755 let declared = base_report(); let md = render_hard_required(&declared);
2757 assert!(
2758 md.contains("**Coverage semantics:** exhaustive\n"),
2759 "declared value renders bare: {md}"
2760 );
2761 assert!(
2762 !md.contains("(resolved from the sources' media"),
2763 "no resolution marker on a declared value: {md}"
2764 );
2765 }
2766}
2767
2768#[cfg(test)]
2769mod rollup_tests {
2770 use super::*;
2771
2772 fn clean_report() -> FidelityReport {
2776 FidelityReport {
2777 legacy_dialect_patterns: Vec::new(),
2778 binding: "engine/graph".to_string(),
2779 destination_mem: "engine".to_string(),
2780 adopt: false,
2781 coverage_semantics: CoverageSemantics::Exhaustive,
2782 coverage_semantics_declared: true,
2783 capabilities: vec![FacetCapability {
2784 facet: "src".to_string(),
2785 medium_type: "codebase".to_string(),
2786 enumerable: true,
2787 change_signal: true,
2788 base_version_retrievable: true,
2789 anchor_namespace: "path".to_string(),
2790 signal: "git".to_string(),
2791 }],
2792 freshness: vec![FacetFreshness {
2793 facet: "src".to_string(),
2794 signal: "git".to_string(),
2795 synced: Some("deadbeef".to_string()),
2796 verified: None,
2797 change_detectable: true,
2798 }],
2799 source_moved_past_synced: Some(false),
2800 coverage: GrainCoverage {
2801 denominator: DenominatorBasis::Enumerated { count: 4 },
2802 covered_artifacts: 4,
2803 describing_entities: 2,
2804 unit: COVERAGE_UNIT,
2805 direct_covered: 4,
2806 tree_only_covered: 0,
2807 uncovered: Vec::new(),
2808 excluded: 0,
2809 tree_anchors: Vec::new(),
2810 },
2811 anchors: AnchorComposition {
2812 by_class: BTreeMap::from([("anchored".to_string(), 4)]),
2813 by_grain: BTreeMap::from([("file".to_string(), 4)]),
2814 authored: 0,
2815 observed: 4,
2816 resolves: 4,
2817 drifted: 0,
2818 recheck: 0,
2819 orphaned: 0,
2820 unobserved: 0,
2821 ..Default::default()
2822 },
2823 findings_by_class: BTreeMap::new(),
2824 backlog: 0,
2825 superseded: Vec::new(),
2826 disposed_excluded: 0,
2827 disposed_excluded_rationales: Vec::new(),
2828 degradations: Vec::new(),
2829 }
2830 }
2831
2832 #[test]
2838 fn unadjudicated_rows_block_clean_but_exclusions_do_not() {
2839 let mut r = clean_report();
2840 assert_eq!(
2841 r.rollup().verdict,
2842 RollupVerdict::Clean,
2843 "the baseline is clean"
2844 );
2845
2846 r.anchors.excluded_out_of_scope = 3;
2848 r.anchors.excluded_other_binding = 2;
2849 r.anchors.excluded_artifacts = vec!["src/a.rs (out-of-scope)".into()];
2850 assert_eq!(
2851 r.rollup().verdict,
2852 RollupVerdict::Clean,
2853 "excluding a row this binding does not answer for is an ANSWER, not a blind spot"
2854 );
2855
2856 let mut unobserved = r.clone();
2858 unobserved.anchors.unobserved = 1;
2859 let roll = unobserved.rollup();
2860 assert_eq!(roll.verdict, RollupVerdict::Inconclusive);
2861 assert!(
2862 roll.blind_spots
2863 .iter()
2864 .any(|b| b.contains("could not be observed")),
2865 "and it names itself: {:?}",
2866 roll.blind_spots
2867 );
2868
2869 let mut span = r.clone();
2871 span.anchors.span_unvalidated = 2;
2872 assert_eq!(span.rollup().verdict, RollupVerdict::Inconclusive);
2873
2874 let mut ent = r.clone();
2876 ent.anchors.unreconciled = Some("the mem's lazy entity load has not run".into());
2877 assert_eq!(ent.rollup().verdict, RollupVerdict::Inconclusive);
2878 }
2879
2880 #[test]
2885 fn all_five_conditions_are_reachable_in_the_report() {
2886 let mut r = clean_report();
2887 r.anchors.excluded_out_of_scope = 1;
2888 r.anchors.excluded_other_binding = 1;
2889 r.anchors.excluded_artifacts = vec![
2890 "src/a.rs (out-of-scope)".into(),
2891 "src/b.rs (other-binding)".into(),
2892 ];
2893 r.anchors.dangling = 1;
2894 r.anchors.dangling_rows = vec!["engine--gone → src/c.rs".into()];
2895 r.anchors.span_unvalidated = 1;
2896 r.anchors.hash_from_backfill = 1;
2897
2898 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
2899 for (needle, condition) in [
2900 ("outside this binding's declared scope", "scope-excluded"),
2901 ("written by another binding", "other-binding"),
2902 ("no longer holds", "dangling entity"),
2903 ("never checked against their artifact", "span not validated"),
2904 ("inferred by backfill", "baseline established by backfill"),
2905 ] {
2906 assert!(
2907 md.contains(needle),
2908 "{condition} is not reachable in the report; looked for {needle:?} in:\n{md}"
2909 );
2910 }
2911 }
2912
2913 #[test]
2915 fn clean_requires_a_substantive_pass_and_no_findings() {
2916 let mut r = clean_report();
2917 assert_eq!(r.rollup().verdict, RollupVerdict::Clean);
2918 assert!(r.rollup().blind_spots.is_empty());
2919 assert!(r.rollup().actions.is_empty());
2920
2921 r.findings_by_class.insert("drifted".to_string(), 2);
2922 let roll = r.rollup();
2923 assert_eq!(roll.verdict, RollupVerdict::Drifted);
2924 assert_eq!(roll.findings_total, 2);
2925 assert!(
2926 roll.actions[0].contains("moved since the entity was written"),
2927 "the top action is the concrete next step: {:?}",
2928 roll.actions
2929 );
2930 }
2931
2932 #[test]
2937 fn a_vacuous_zero_over_zero_is_inconclusive_not_clean() {
2938 let mut r = clean_report();
2939 r.coverage.denominator = DenominatorBasis::Enumerated { count: 0 };
2940 let roll = r.rollup();
2941 assert_eq!(
2942 roll.verdict,
2943 RollupVerdict::Inconclusive,
2944 "0/0 is not a clean bill of health"
2945 );
2946 assert!(
2947 roll.blind_spots.iter().any(|s| s.contains("vacuous")),
2948 "the blindness is named, not implied: {:?}",
2949 roll.blind_spots
2950 );
2951 }
2952
2953 #[test]
2957 fn a_non_enumerable_facet_blocks_green_even_in_a_mixed_binding() {
2958 let mut r = clean_report();
2959 r.capabilities.push(FacetCapability {
2960 facet: "site".to_string(),
2961 medium_type: "web".to_string(),
2962 enumerable: false,
2963 change_signal: true,
2967 base_version_retrievable: false,
2968 anchor_namespace: "url".to_string(),
2969 signal: "none".to_string(),
2970 });
2971 assert!(matches!(
2973 r.coverage.denominator,
2974 DenominatorBasis::Enumerated { count } if count > 0
2975 ));
2976 let roll = r.rollup();
2977 assert_eq!(
2978 roll.verdict,
2979 RollupVerdict::Inconclusive,
2980 "one enumerable facet must not launder a non-enumerable one: {roll:?}"
2981 );
2982 assert!(
2983 roll.blind_spots
2984 .iter()
2985 .any(|s| s.contains("not enumerable")),
2986 "{:?}",
2987 roll.blind_spots
2988 );
2989 }
2990
2991 #[test]
2997 fn a_resolved_signal_of_none_blocks_green_even_when_the_medium_could_signal() {
2998 let mut r = clean_report();
2999 r.capabilities[0].change_signal = true;
3002 r.capabilities[0].signal = "none".to_string();
3003 r.freshness[0].change_detectable = false;
3004 r.freshness[0].signal = "none".to_string();
3005 let roll = r.rollup();
3006 assert_eq!(
3007 roll.verdict,
3008 RollupVerdict::Inconclusive,
3009 "a change-blind binding is not a clean bill of health: {roll:?}"
3010 );
3011 assert!(
3012 roll.blind_spots
3013 .iter()
3014 .any(|s| s.contains("could not read that signal")),
3015 "the blind spot names the unreadable signal: {:?}",
3016 roll.blind_spots
3017 );
3018 }
3019
3020 #[test]
3024 fn a_facet_without_a_change_signal_blocks_green() {
3025 let mut r = clean_report();
3026 r.capabilities[0].change_signal = false;
3027 let roll = r.rollup();
3028 assert_eq!(roll.verdict, RollupVerdict::Inconclusive);
3029 assert!(
3030 roll.blind_spots
3031 .iter()
3032 .any(|s| s.contains("no change signal")),
3033 "{:?}",
3034 roll.blind_spots
3035 );
3036 }
3037
3038 #[test]
3041 fn a_non_enumerable_scope_blocks_green() {
3042 let mut r = clean_report();
3043 r.coverage.denominator = DenominatorBasis::NonEnumerable {
3044 reason: "web medium".to_string(),
3045 };
3046 assert_eq!(r.rollup().verdict, RollupVerdict::Inconclusive);
3047 }
3048
3049 #[test]
3051 fn zero_observed_anchors_blocks_green() {
3052 let mut r = clean_report();
3053 r.anchors.observed = 0;
3054 r.anchors.resolves = 0;
3055 assert_eq!(r.rollup().verdict, RollupVerdict::Inconclusive);
3056 }
3057
3058 #[test]
3062 fn adopt_with_only_uncovered_is_never_red() {
3063 let mut r = clean_report();
3064 r.adopt = true;
3065 r.findings_by_class.insert("uncovered".to_string(), 12);
3066 let roll = r.rollup();
3067 assert_eq!(
3068 roll.verdict,
3069 RollupVerdict::Inconclusive,
3070 "onboarding is neither drift nor a clean bill: {roll:?}"
3071 );
3072 assert!(
3073 roll.because.contains("backfill worklist"),
3074 "the reason states the onboarding framing: {}",
3075 roll.because
3076 );
3077
3078 r.findings_by_class.insert("drifted".to_string(), 1);
3081 assert_eq!(r.rollup().verdict, RollupVerdict::Drifted);
3082 }
3083
3084 #[test]
3087 fn findings_outrank_blind_spots() {
3088 let mut r = clean_report();
3089 r.capabilities[0].change_signal = false;
3090 r.findings_by_class.insert("wrong".to_string(), 1);
3091 let roll = r.rollup();
3092 assert_eq!(roll.verdict, RollupVerdict::Drifted);
3093 assert!(
3094 !roll.blind_spots.is_empty(),
3095 "the blindness is still reported alongside the verdict"
3096 );
3097 }
3098
3099 #[test]
3102 fn actions_are_severity_ordered_and_never_drop_a_class() {
3103 let mut r = clean_report();
3104 r.findings_by_class.insert("uncovered".to_string(), 3);
3105 r.findings_by_class.insert("wrong".to_string(), 1);
3106 r.findings_by_class
3107 .insert("some-future-class".to_string(), 2);
3108 let roll = r.rollup();
3109 assert!(
3110 roll.actions[0].contains("contradict their source"),
3111 "{roll:?}"
3112 );
3113 assert_eq!(roll.actions.len(), 3, "{roll:?}");
3114 assert!(
3115 roll.actions.iter().any(|a| a.contains("some-future-class")),
3116 "an unranked class still surfaces: {roll:?}"
3117 );
3118 }
3119
3120 #[test]
3122 fn verdict_wire_strings_are_stable() {
3123 assert_eq!(RollupVerdict::Clean.wire(), "clean");
3124 assert_eq!(RollupVerdict::Drifted.wire(), "drifted");
3125 assert_eq!(RollupVerdict::Inconclusive.wire(), "inconclusive");
3126 let json = serde_json::to_string(&RollupVerdict::Inconclusive).unwrap();
3127 assert_eq!(json, "\"inconclusive\"");
3128 }
3129
3130 fn a3_workspace(root: &std::path::Path, files: &[&str]) {
3134 let mem_dir = root.join("mem");
3135 std::fs::create_dir_all(mem_dir.join(".memstead")).unwrap();
3136 std::fs::write(
3137 mem_dir.join(".memstead").join("config.json"),
3138 r#"{"format":1,"schema":"default@1.0.0","version":"1.0.0"}"#,
3139 )
3140 .unwrap();
3141 std::fs::create_dir_all(root.join(".memstead")).unwrap();
3142 std::fs::write(
3143 root.join(".memstead").join("workspace.toml"),
3144 "format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
3145 )
3146 .unwrap();
3147 let mount = crate::workspace::Mount {
3148 mem: "engine".to_string(),
3149 schema: Some("default@1.0.0".parse().unwrap()),
3150 storage: crate::workspace::MountStorage::Folder {
3151 path: mem_dir.clone(),
3152 },
3153 capability: crate::workspace::MountCapability::Write,
3154 lifecycle: crate::workspace::MountLifecycle::Eager,
3155 cross_linkable: false,
3156 migration_target: None,
3157 };
3158 crate::workspace_store::WorkspaceStoreAdapter::save_state(
3159 &crate::FileWorkspaceStore::new(),
3160 root,
3161 &crate::workspace::Workspace {
3162 mounts: vec![mount],
3163 settings: crate::workspace::WorkspaceSettings::default(),
3164 },
3165 )
3166 .unwrap();
3167 let out = std::process::Command::new("git")
3168 .args(["init", "-q"])
3169 .current_dir(root)
3170 .output()
3171 .unwrap();
3172 assert!(out.status.success());
3173 for f in files {
3174 let p = root.join(f);
3175 std::fs::create_dir_all(p.parent().unwrap()).unwrap();
3176 std::fs::write(p, "fn x() {}\n").unwrap();
3177 }
3178 }
3179
3180 fn a3_binding(
3181 sources: &[(&str, &str)],
3182 deny: &[&str],
3183 batch_size: u32,
3184 ) -> crate::binding::Binding {
3185 crate::binding::Binding {
3186 version: crate::binding::BINDING_VERSION,
3187 intent: None,
3188 sources: sources
3189 .iter()
3190 .map(|(name, glob)| crate::pipeline::Source {
3191 name: name.to_string(),
3192 medium_type: crate::pipeline::MediumType::Codebase,
3193 pointer: String::new(),
3194 change_detection: Some("git".to_string()),
3195 scope: vec![crate::pipeline::PatternEntry {
3196 path: glob.to_string(),
3197 mode: crate::pipeline::PatternMode::Allow,
3198 }],
3199 engagement: None,
3200 preparation: None,
3201 })
3202 .collect(),
3203 reference_mems: Vec::new(),
3204 destination_mem: "engine".to_string(),
3205 deny_paths: deny.iter().map(|d| d.to_string()).collect(),
3206 coverage_semantics: None,
3207 rules: None,
3208 prune: None,
3209 operations: crate::binding::Operations {
3210 build: Some(crate::binding::BuildOperation {
3211 mode: crate::binding::BuildMode::Discovery,
3212 trigger: crate::pipeline::IngestTrigger::Loop,
3213 batch_size,
3214 post_actions: None,
3215 }),
3216 sync: None,
3217 verify: Some(crate::binding::VerifyOperation {
3218 trigger: crate::pipeline::IngestTrigger::Manual,
3219 batch_size,
3220 adjudication_cap: crate::binding::DEFAULT_ADJUDICATION_CAP,
3221 full_resync_every: 0,
3223 }),
3224 },
3225 }
3226 }
3227
3228 #[test]
3232 fn coverage_counts_artifacts_described_not_anchor_rows() {
3233 let tmp = tempfile::tempdir().unwrap();
3234 let root = tmp.path();
3235 a3_workspace(root, &["src/three.rs", "src/two.rs", "src/none.rs"]);
3236 let mem_dir = root.join("mem");
3237 for slug in ["one", "left", "right"] {
3238 std::fs::write(
3239 mem_dir.join(format!("{slug}.md")),
3240 "---\ntype: decision\n---\n\n# E\n\n## Decision\n\nBody.\n",
3241 )
3242 .unwrap();
3243 }
3244 let mk = |artifact: &str| crate::anchor::Anchor {
3245 artifact: artifact.to_string(),
3246 grain: crate::anchor::AnchorGrain::File,
3247 class: crate::anchor::AnchorProvenanceClass::Anchored,
3248 at_version: None,
3249 hash: Some("recorded".to_string()),
3250 hash_stability: crate::anchor::AnchorHashStability::Stable,
3251 derived_from: Vec::new(),
3252 binding: None,
3253 source: None,
3254 span_unvalidated: false,
3255 hash_source: None,
3256 last_observed: None,
3257 };
3258 let mut sidecar = crate::anchor::AnchorSidecar::default();
3259 sidecar.set(
3260 "engine--one",
3261 vec![mk("src/three.rs"), mk("src/three.rs"), mk("src/three.rs")],
3262 );
3263 sidecar.set("engine--left", vec![mk("src/two.rs")]);
3264 sidecar.set("engine--right", vec![mk("src/two.rs")]);
3265 std::fs::write(
3266 mem_dir.join(crate::anchor::ANCHOR_SIDECAR_PATH),
3267 sidecar.to_bytes(),
3268 )
3269 .unwrap();
3270 let b = a3_binding(&[("graph", "src/**/*.rs")], &[], 20);
3271 crate::pipeline_store::write_binding(root, "engine", "graph", &b).unwrap();
3272
3273 let engine = crate::Engine::from_workspace_root(root).unwrap();
3274 let resolved = crate::ingest::resolve::resolve_binding_run("engine/graph", &b).unwrap();
3275 let outcome =
3276 crate::ingest::findings::verify_binding(&engine, root, &b, &resolved).unwrap();
3277 let report = super::compute_fidelity_report(&engine, root, &b, &resolved, &outcome.key);
3278 assert_eq!(
3279 report.coverage.denominator,
3280 super::DenominatorBasis::Enumerated { count: 3 }
3281 );
3282 assert_eq!(
3283 report.coverage.covered_artifacts, 2,
3284 "{:?}",
3285 report.coverage
3286 );
3287 assert_eq!(
3288 report.coverage.describing_entities, 3,
3289 "{:?}",
3290 report.coverage
3291 );
3292 assert_eq!(report.coverage.uncovered, vec!["src/none.rs".to_string()]);
3293 let md = super::render_fidelity_report(&report, 8_000, &[]).markdown;
3294 assert!(
3295 md.contains("coverage unit: describing entities per artifact"),
3296 "{md}"
3297 );
3298 assert!(
3299 md.contains("3 describing entities over 2 covered artifact(s)"),
3300 "{md}"
3301 );
3302 }
3303}