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
115#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
119pub struct GrainCoverage {
120 pub denominator: DenominatorBasis,
122 pub direct_covered: usize,
124 pub tree_only_covered: usize,
127 pub uncovered: Vec<String>,
129 pub tree_anchors: Vec<TreeFanout>,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
137pub struct AnchorComposition {
138 pub by_class: BTreeMap<String, usize>,
142 pub by_grain: BTreeMap<String, usize>,
144 pub authored: usize,
147 pub observed: usize,
149 pub resolves: usize,
151 pub drifted: usize,
153 pub recheck: usize,
155 pub orphaned: usize,
157 pub unobserved: usize,
160 pub counted_rows: usize,
166 pub distinct_artifacts: usize,
171 pub excluded_other_binding: usize,
174 pub excluded_out_of_scope: usize,
177 pub excluded_artifacts: Vec<String>,
181 pub counted_without_provenance: usize,
185 pub dangling: usize,
190 pub dangling_rows: Vec<String>,
194 pub unreconciled: Option<String>,
198 pub span_unvalidated: usize,
203 pub hash_from_backfill: usize,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
213pub struct FacetCapability {
214 pub facet: String,
216 pub medium_type: String,
218 pub enumerable: bool,
220 pub change_signal: bool,
222 pub base_version_retrievable: bool,
224 pub anchor_namespace: String,
226 pub signal: String,
229}
230
231impl FacetCapability {
232 fn from_caps(
233 facet: String,
234 medium_type: String,
235 caps: MediumCapabilities,
236 strategy: ChangeStrategy,
237 ) -> Self {
238 FacetCapability {
239 facet,
240 medium_type,
241 enumerable: caps.enumerable,
242 change_signal: caps.change_signal,
243 base_version_retrievable: caps.base_version_retrievable
250 && strategy_retrieves_base(strategy),
251 anchor_namespace: caps.anchor_namespace.to_string(),
252 signal: signal_wire(strategy).to_string(),
253 }
254 }
255}
256
257#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
259pub struct FacetFreshness {
260 pub facet: String,
262 pub signal: String,
264 pub synced: Option<String>,
266 pub verified: Option<String>,
268 pub change_detectable: bool,
273}
274
275#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
277pub struct FidelityReport {
278 pub binding: String,
280 pub destination_mem: String,
282 pub adopt: bool,
289 pub coverage_semantics: CoverageSemantics,
293 pub coverage_semantics_declared: bool,
298 pub legacy_dialect_patterns: Vec<String>,
304 pub capabilities: Vec<FacetCapability>,
306 pub freshness: Vec<FacetFreshness>,
308 pub source_moved_past_synced: Option<bool>,
312 pub coverage: GrainCoverage,
314 pub anchors: AnchorComposition,
316 pub findings_by_class: BTreeMap<String, usize>,
318 pub backlog: usize,
320 pub superseded: Vec<String>,
323 pub disposed_excluded: usize,
326 pub disposed_excluded_rationales: Vec<(String, String)>,
332 pub degradations: Vec<String>,
334}
335
336#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
352#[serde(rename_all = "lowercase")]
353pub enum RollupVerdict {
354 Clean,
356 Drifted,
358 Inconclusive,
361}
362
363impl RollupVerdict {
364 pub fn wire(&self) -> &'static str {
366 match self {
367 RollupVerdict::Clean => "clean",
368 RollupVerdict::Drifted => "drifted",
369 RollupVerdict::Inconclusive => "inconclusive",
370 }
371 }
372}
373
374#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
377pub struct Rollup {
378 pub verdict: RollupVerdict,
380 pub findings_total: usize,
382 pub because: String,
385 pub blind_spots: Vec<String>,
390 pub actions: Vec<String>,
393}
394
395const CLASS_SEVERITY: [&str; 5] = [
400 "wrong",
401 "drifted",
402 "unresolvable-anchor",
403 "uncovered",
404 "queued-for-adjudication",
405];
406
407fn class_action(class: &str, n: usize, binding: &str) -> String {
409 match class {
410 "wrong" => format!(
411 "{n} entity/entities contradict their source — read them against the source and \
412 correct the entity (`memstead projection brief {binding}` lists them)"
413 ),
414 "drifted" => format!(
415 "{n} anchored artifact(s) moved since the entity was written — re-read the source \
416 and update the entity, then re-verify to advance the baseline"
417 ),
418 "unresolvable-anchor" => format!(
419 "{n} anchor(s) no longer resolve to anything — repoint them at the artifact's new \
420 location or unset them (`memstead_update` `anchors_unset`)"
421 ),
422 "uncovered" => format!(
423 "{n} in-scope source artifact(s) carry no anchor — cover them via \
424 `memstead projection brief {binding} --sync`, or record a disposition for the \
425 ones deliberately excluded"
426 ),
427 "queued-for-adjudication" => format!(
428 "{n} finding(s) are queued and not yet adjudicated — run \
429 `memstead projection verify {binding} --full` to work the backlog down"
430 ),
431 other => format!("{n} `{other}` finding(s) recorded"),
432 }
433}
434
435impl FidelityReport {
436 pub fn rollup(&self) -> Rollup {
443 let findings_total: usize = self.findings_by_class.values().sum();
444
445 let mut blind_spots: Vec<String> = Vec::new();
446 match &self.coverage.denominator {
447 DenominatorBasis::NonEnumerable { reason } => blind_spots.push(format!(
448 "the source scope is not enumerable ({reason}) — coverage is reported over \
449 anchors only, so an uncovered artifact cannot be detected"
450 )),
451 DenominatorBasis::Enumerated { count: 0 } => blind_spots.push(
452 "the enumerated source scope is empty (0 artifacts) — every coverage figure \
453 below is vacuous, not clean"
454 .to_string(),
455 ),
456 DenominatorBasis::Partial { count, reason } => blind_spots.push(format!(
457 "the source enumeration is INCOMPLETE ({reason}) — {count} artifact(s) \
458 survived, but their share of the population is unknown, so no coverage \
459 percentage is reported below"
460 )),
461 DenominatorBasis::Enumerated { .. } => {}
462 }
463 if !self.legacy_dialect_patterns.is_empty() {
464 blind_spots.push(format!(
465 "scope pattern(s) are still written against the workspace root rather than the \
466 source pointer and select nothing under the pointer join, so whatever they \
467 were meant to cover is absent from the denominator: {}. Rewrite them relative \
468 to the source's pointer",
469 self.legacy_dialect_patterns.join(", ")
470 ));
471 }
472 if self.anchors.observed == 0 {
473 blind_spots.push(
474 "no anchor carried a resolution state this pass — nothing was adjudicated"
475 .to_string(),
476 );
477 }
478 if self.anchors.unobserved > 0 {
490 blind_spots.push(format!(
491 "{} counted anchor(s) could not be observed at all this pass, so their state is unknown rather than clean",
492 self.anchors.unobserved
493 ));
494 }
495 if self.anchors.span_unvalidated > 0 {
496 blind_spots.push(format!(
497 "{} counted span anchor(s) were never checked against their artifact, so the span they name is unverified even where the hash resolves",
498 self.anchors.span_unvalidated
499 ));
500 }
501 if let Some(why) = &self.anchors.unreconciled {
502 blind_spots.push(format!(
503 "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"
504 ));
505 }
506 let change_blind: std::collections::BTreeSet<&str> = self
516 .freshness
517 .iter()
518 .filter(|f| !f.change_detectable)
519 .map(|f| f.facet.as_str())
520 .collect();
521 for cap in &self.capabilities {
522 if !cap.change_signal {
523 blind_spots.push(format!(
524 "facet `{}` ({}) provides no change signal — drift on it cannot be \
525 observed at all",
526 cap.facet, cap.medium_type
527 ));
528 } else if change_blind.contains(cap.facet.as_str()) {
529 blind_spots.push(format!(
530 "facet `{}` ({}) declares change-detection `{}` but this pass could \
531 not read that signal — either the binding asked for none, or the \
532 checkout cannot deliver it (a `git` source with no `.git`: an \
533 archive, a container COPY, a vendored drop). Drift on it cannot \
534 be observed",
535 cap.facet, cap.medium_type, cap.signal
536 ));
537 }
538 if !cap.enumerable {
548 blind_spots.push(format!(
549 "facet `{}` ({}) is not enumerable — an uncovered artifact under it \
550 cannot be detected, only an anchored one",
551 cap.facet, cap.medium_type
552 ));
553 }
554 }
555
556 let mut actions: Vec<String> = Vec::new();
557 for class in CLASS_SEVERITY {
558 if let Some(&n) = self.findings_by_class.get(class)
559 && n > 0
560 {
561 actions.push(class_action(class, n, &self.binding));
562 }
563 }
564 for (class, &n) in &self.findings_by_class {
567 if n > 0 && !CLASS_SEVERITY.contains(&class.as_str()) {
568 actions.push(class_action(class, n, &self.binding));
569 }
570 }
571
572 let only_uncovered = findings_total > 0
578 && self
579 .findings_by_class
580 .iter()
581 .all(|(class, &n)| n == 0 || class == "uncovered");
582
583 let (verdict, because) = if self.adopt && only_uncovered {
584 (
585 RollupVerdict::Inconclusive,
586 format!(
587 "this mem predates its binding — the {findings_total} uncovered artifact(s) \
588 are the backfill worklist, not drift"
589 ),
590 )
591 } else if findings_total > 0 {
592 let tally = self
593 .findings_by_class
594 .iter()
595 .filter(|(_, n)| **n > 0)
596 .map(|(class, n)| format!("{class}: {n}"))
597 .collect::<Vec<_>>()
598 .join(", ");
599 (
600 RollupVerdict::Drifted,
601 format!("{findings_total} finding(s) recorded over the current key ({tally})"),
602 )
603 } else if !blind_spots.is_empty() {
604 (
605 RollupVerdict::Inconclusive,
606 format!(
607 "no findings recorded, but the pass could not speak to {} axis/axes — \
608 this is not a clean bill of health",
609 blind_spots.len()
610 ),
611 )
612 } else {
613 (
614 RollupVerdict::Clean,
615 "the pass was substantive on every axis and recorded no findings".to_string(),
616 )
617 };
618
619 Rollup {
620 verdict,
621 findings_total,
622 because,
623 blind_spots,
624 actions,
625 }
626 }
627}
628
629#[derive(Debug, Clone, PartialEq, Eq)]
636pub struct RenderedFidelityReport {
637 pub markdown: String,
639 pub mode: String,
642 pub hints: Vec<(String, usize)>,
645 pub budget_used: usize,
647}
648
649fn ratio(num: usize, den: usize) -> String {
655 if den == 0 {
656 format!("{num}/{den} (n/a)")
657 } else {
658 let pct = (num as f64) * 100.0 / (den as f64);
659 format!("{num}/{den} ({pct:.1}%)")
660 }
661}
662
663fn render_hard_required(report: &FidelityReport) -> String {
667 let mut md = String::new();
668 md.push_str(&format!("# Fidelity report — `{}`\n\n", report.binding));
669
670 let rollup = report.rollup();
675 md.push_str(&format!(
676 "**Verdict: {}** — {}.\n\n",
677 rollup.verdict.wire().to_uppercase(),
678 rollup.because
679 ));
680 if !rollup.actions.is_empty() {
681 md.push_str("**Do next:**\n\n");
682 for action in &rollup.actions {
683 md.push_str(&format!("1. {action}\n"));
684 }
685 md.push('\n');
686 }
687 if !rollup.blind_spots.is_empty() {
688 md.push_str("**This pass could not see:**\n\n");
689 for spot in &rollup.blind_spots {
690 md.push_str(&format!("- {spot}\n"));
691 }
692 md.push('\n');
693 }
694
695 md.push_str(&format!(
696 "- **Destination mem:** `{}`\n- **Coverage semantics:** {}{}\n\n",
697 report.destination_mem,
698 match report.coverage_semantics {
699 CoverageSemantics::Exhaustive => "exhaustive",
700 CoverageSemantics::Curated => "curated",
701 },
702 if report.coverage_semantics_declared {
703 ""
704 } else {
705 " (resolved from the sources' media — not declared)"
706 }
707 ));
708
709 if report.adopt {
716 md.push_str("## Adopting — first verify\n\n");
717 md.push_str(
718 "This mem predates its binding: it carries no anchors and has no prior sync \
719 baseline, so **0% anchored is expected — this is onboarding, not a failure.** \
720 Do not read the coverage numbers below as drift or a red verdict; the uncovered \
721 artifacts are the backfill worklist, not defects.\n\n",
722 );
723 md.push_str(&format!(
724 "**Backfill path:** run `memstead projection brief {} --sync` to work through the in-scope \
725 source artifacts that carry no entity yet, covering the clearly-new concepts among \
726 them through the normal mutation surface. Backfilling is incremental — a partial \
727 pass is fine, and the next sync continues where you left off.\n\n",
728 report.binding
729 ));
730 }
731
732 md.push_str("## Denominator provenance\n\n");
734 match &report.coverage.denominator {
735 DenominatorBasis::Enumerated { count } => md.push_str(&format!(
736 "Coverage is reported relative to the per-medium enumeration `S(D)` = **{count}** \
737 source artifact(s) in scope (after `deny_paths`).\n\n"
738 )),
739 DenominatorBasis::NonEnumerable { reason } => md.push_str(&format!(
740 "No `S(D)` denominator: {reason}. Coverage is reported over anchors only; the \
741 per-medium enumeration is unavailable.\n\n"
742 )),
743 DenominatorBasis::Partial { count, reason } => md.push_str(&format!(
744 "`S(D)` is **partial**: {reason}. **{count}** source artifact(s) were \
745 enumerated by the patterns that did resolve, but that set is not the \
746 population, so the coverage figures below are counts and carry no \
747 percentage.\n\n"
748 )),
749 }
750
751 md.push_str("## Capability matrix\n\n");
753 if report.capabilities.is_empty() {
754 md.push_str("_(no primary sources resolved)_\n\n");
755 } else {
756 for c in &report.capabilities {
757 md.push_str(&format!("### `{}` ({})\n\n", c.facet, c.medium_type));
758 md.push_str(&format!(
759 "- enumerable: {} | change_signal: {} | base_version_retrievable: {}\n",
760 c.enumerable, c.change_signal, c.base_version_retrievable
761 ));
762 md.push_str(&format!(
763 "- anchor_namespace: `{}` | resolved signal: `{}`\n\n",
764 c.anchor_namespace, c.signal
765 ));
766 }
767 }
768
769 md.push_str("## Freshness\n\n");
771 if report.freshness.is_empty() {
772 md.push_str("_(no source facets)_\n\n");
773 } else {
774 for f in &report.freshness {
775 md.push_str(&format!("### `{}`\n\n", f.facet));
776 md.push_str(&format!("- signal: `{}`\n", f.signal));
777 if !f.change_detectable {
778 md.push_str(
782 "- **freshness unknowable** — this medium is not change-detectable \
783 (no change signal); `#synced` / `#verified` cannot be adjudicated as fresh\n",
784 );
785 } else {
786 match &f.synced {
787 Some(t) => md.push_str(&format!("- `#synced`: `{t}`\n")),
788 None => md.push_str("- `#synced`: never synced\n"),
789 }
790 match &f.verified {
791 Some(t) => md.push_str(&format!("- `#verified`: `{t}`\n")),
792 None => md.push_str("- `#verified`: never verified\n"),
793 }
794 }
795 md.push('\n');
796 }
797 match report.source_moved_past_synced {
799 Some(true) => md.push_str(
800 "**Source moved past its `#synced` baseline** — the graph is stale for the \
801 moved facet(s); a sync pass is due.\n\n",
802 ),
803 Some(false) => {
804 md.push_str("Every change-detectable source is at its `#synced` baseline.\n\n")
805 }
806 None => {}
807 }
808 }
809
810 md.push_str("## Coverage (grain-classed)\n\n");
812 let den = match &report.coverage.denominator {
816 DenominatorBasis::Enumerated { count } => *count,
817 DenominatorBasis::NonEnumerable { .. } | DenominatorBasis::Partial { .. } => 0,
818 };
819 md.push_str(&format!(
820 "- direct-covered (file / span anchors): {}\n",
821 ratio(report.coverage.direct_covered, den)
822 ));
823 let tree_files: usize = report.coverage.tree_anchors.iter().map(|t| t.fanout).sum();
826 md.push_str(&format!(
827 "- tree-anchor fan-out (separate axis): {} tree anchor(s) fanning out over {} file(s); \
828 {} file(s) covered ONLY via a tree anchor\n",
829 report.coverage.tree_anchors.len(),
830 tree_files,
831 report.coverage.tree_only_covered
832 ));
833 md.push_str(&format!(
834 "- uncovered (no anchor): {}\n\n",
835 report.coverage.uncovered.len()
836 ));
837
838 match report.coverage_semantics {
843 CoverageSemantics::Exhaustive if report.adopt => {
844 let backlog = report
845 .coverage
846 .uncovered
847 .len()
848 .saturating_sub(report.disposed_excluded);
849 md.push_str(&format!(
850 "**Exhaustive coverage (onboarding):** {backlog} in-scope artifact(s) carry no \
851 entity yet ({} disposed excluded) — the expected first-sync backfill worklist \
852 for a mem that predates its binding, not defects.\n\n",
853 report.disposed_excluded
854 ));
855 }
856 CoverageSemantics::Exhaustive => {
857 let findings = report
858 .coverage
859 .uncovered
860 .len()
861 .saturating_sub(report.disposed_excluded);
862 md.push_str(&format!(
863 "**Exhaustive coverage:** {findings} unaccounted artifact(s) — not anchored, not \
864 declared-excluded, no persisted disposition ({} disposed excluded) — are \
865 **findings**.\n\n",
866 report.disposed_excluded
867 ));
868 }
869 CoverageSemantics::Curated => {
870 md.push_str(&format!(
871 "**Curated coverage:** {} unaccounted artifact(s) are **information**, not \
872 defects — a curated binding covers a deliberate slice.\n\n",
873 report.coverage.uncovered.len()
874 ));
875 }
876 }
877
878 if !report.disposed_excluded_rationales.is_empty() {
882 md.push_str("**Excluded on purpose (persisted dispositions):**\n");
883 for (artifact, rationale) in &report.disposed_excluded_rationales {
884 if rationale.is_empty() {
885 md.push_str(&format!("- `{artifact}`\n"));
886 } else {
887 md.push_str(&format!("- `{artifact}` — {rationale}\n"));
888 }
889 }
890 md.push('\n');
891 }
892
893 md.push_str("## Anchors\n\n");
895 md.push_str(&format!(
896 "- by class: {}\n",
897 render_counts(&report.anchors.by_class)
898 ));
899 md.push_str(&format!(
900 "- by grain: {}\n",
901 render_counts(&report.anchors.by_grain)
902 ));
903 md.push_str(&format!(
904 "- `authored` bucket (excluded from coverage/accuracy denominators): {}\n",
905 report.anchors.authored
906 ));
907 md.push_str(&format!(
914 "- resolution (non-`authored`, observed): resolves {}, drifted {}, recheck {}, \
915 orphaned {}; **anchor-resolution %:** {} over {} counted row(s) on {} distinct \
916 artifact(s), with {} unobserved this pass (state unavailable, never scored as \
917 resolved)\n",
918 report.anchors.resolves,
919 report.anchors.drifted,
920 report.anchors.recheck,
921 report.anchors.orphaned,
922 ratio(report.anchors.resolves, report.anchors.observed),
923 report.anchors.counted_rows,
924 report.anchors.distinct_artifacts,
925 report.anchors.unobserved
926 ));
927 md.push_str(&format!(
933 "- the figures above count anchor ROWS: {} row(s) over {} distinct artifact(s)\n",
934 report.anchors.counted_rows, report.anchors.distinct_artifacts
935 ));
936 if report.anchors.excluded_other_binding > 0 || report.anchors.excluded_out_of_scope > 0 {
939 md.push_str(&format!(
940 "- excluded from this binding's population: {} written by another binding, \
941 {} outside this binding's declared scope (legal, reported here, never deleted)\n",
942 report.anchors.excluded_other_binding, report.anchors.excluded_out_of_scope
943 ));
944 const NAMED_CAP: usize = 10;
951 for a in report.anchors.excluded_artifacts.iter().take(NAMED_CAP) {
952 md.push_str(&format!(" - {a}\n"));
953 }
954 if report.anchors.excluded_artifacts.len() > NAMED_CAP {
955 md.push_str(&format!(
956 " - …and {} more (counts above are complete)\n",
957 report.anchors.excluded_artifacts.len() - NAMED_CAP
958 ));
959 }
960 }
961 match (&report.anchors.unreconciled, report.anchors.dangling) {
966 (Some(why), _) => md.push_str(&format!(
967 "- the entity end of these anchors was NOT reconciled this pass ({why}), so \
968 dangling sidecar rows would not have been detected\n"
969 )),
970 (None, 0) => {}
971 (None, n) => {
972 md.push_str(&format!(
973 "- {n} sidecar row(s) name an entity this mem no longer holds. Excluded from \
974 every figure above, reported rather than repaired: the row is the trace of a \
975 writer that went around the engine\n"
976 ));
977 const NAMED_CAP: usize = 10;
978 for r in report.anchors.dangling_rows.iter().take(NAMED_CAP) {
979 md.push_str(&format!(" - {r}\n"));
980 }
981 if report.anchors.dangling_rows.len() > NAMED_CAP {
982 md.push_str(&format!(
983 " - …and {} more (the count above is complete)\n",
984 report.anchors.dangling_rows.len() - NAMED_CAP
985 ));
986 }
987 }
988 }
989 if report.anchors.span_unvalidated > 0 {
995 md.push_str(&format!(
996 "- {} counted span row(s) were never checked against their artifact, so their \
997 span is unverified even where the hash resolves\n",
998 report.anchors.span_unvalidated
999 ));
1000 }
1001 if report.anchors.hash_from_backfill > 0 {
1002 md.push_str(&format!(
1003 "- {} counted row(s) carry a baseline the engine inferred by backfill rather than \
1004 one an author pinned\n",
1005 report.anchors.hash_from_backfill
1006 ));
1007 }
1008 if report.anchors.counted_without_provenance > 0 {
1009 md.push_str(&format!(
1010 "- {} counted anchor(s) record no producing binding and are included by the \
1011 pre-provenance fallback, so this population rests partly on that fallback \
1012 rather than wholly on provenance\n",
1013 report.anchors.counted_without_provenance
1014 ));
1015 }
1016 md.push('\n');
1017
1018 md.push_str("## Findings\n\n");
1020 md.push_str(&format!(
1021 "- by class: {}\n",
1022 render_counts(&report.findings_by_class)
1023 ));
1024 md.push_str(&format!(
1025 "- **tier-3 adjudication backlog:** {}\n",
1026 report.backlog
1027 ));
1028 md.push_str(&format!(
1029 "- superseded (prior `hash(D)`, segregated): {}\n\n",
1030 report.superseded.len()
1031 ));
1032
1033 md.push_str("## Degradations\n\n");
1035 if report.degradations.is_empty() {
1036 md.push_str("_(none)_\n\n");
1037 } else {
1038 for d in &report.degradations {
1039 md.push_str(&format!("- {d}\n"));
1040 }
1041 md.push('\n');
1042 }
1043
1044 md
1045}
1046
1047fn render_counts(counts: &BTreeMap<String, usize>) -> String {
1049 if counts.is_empty() {
1050 return "(none)".to_string();
1051 }
1052 counts
1053 .iter()
1054 .map(|(k, v)| format!("{k}={v}"))
1055 .collect::<Vec<_>>()
1056 .join(", ")
1057}
1058
1059fn heavy_sections(report: &FidelityReport) -> Vec<(&'static str, String)> {
1063 let mut out: Vec<(&'static str, String)> = Vec::new();
1064
1065 let mut s = String::new();
1067 if !report.coverage.uncovered.is_empty() {
1068 s.push_str("## Uncovered artifacts\n\n");
1069 for a in &report.coverage.uncovered {
1070 s.push_str(&format!("- `{a}`\n"));
1071 }
1072 s.push('\n');
1073 }
1074 out.push(("uncovered_artifacts", s));
1075
1076 let mut s = String::new();
1078 if !report.coverage.tree_anchors.is_empty() {
1079 s.push_str("## Tree-anchor fan-out (detail)\n\n");
1080 for t in &report.coverage.tree_anchors {
1081 s.push_str(&format!(
1082 "- `{}` → `{}` fans out over {} file(s)\n",
1083 t.entity, t.artifact, t.fanout
1084 ));
1085 }
1086 s.push('\n');
1087 }
1088 out.push(("tree_fanout", s));
1089
1090 let mut s = String::new();
1092 if !report.superseded.is_empty() {
1093 s.push_str("## Superseded findings (detail)\n\n");
1094 for f in &report.superseded {
1095 s.push_str(&format!("- {f}\n"));
1096 }
1097 s.push('\n');
1098 }
1099 out.push(("superseded_findings", s));
1100
1101 out
1102}
1103
1104pub fn render_fidelity_report(
1115 report: &FidelityReport,
1116 budget: usize,
1117 include: &[String],
1118) -> RenderedFidelityReport {
1119 let hard = render_hard_required(report);
1120 let hard_cost = estimate_tokens(&hard);
1121 let overbudget = hard_cost > budget;
1122
1123 let include_set: std::collections::BTreeSet<&str> = include
1124 .iter()
1125 .map(String::as_str)
1126 .filter(|k| ALLOWED_REPORT_INCLUDE_KEYS.contains(k))
1127 .collect();
1128 let unknown_includes: Vec<&String> = include
1129 .iter()
1130 .filter(|k| !ALLOWED_REPORT_INCLUDE_KEYS.contains(&k.as_str()))
1131 .collect();
1132
1133 let sections = heavy_sections(report);
1134 let mut emitted: Vec<String> = Vec::new();
1135 let mut hints: Vec<(String, usize)> = Vec::new();
1136 let mut used = hard_cost;
1137 let mut remaining = budget.saturating_sub(hard_cost);
1138
1139 for (key, section_md) in §ions {
1140 if section_md.is_empty() {
1141 continue; }
1143 let cost = estimate_tokens(section_md);
1144 let forced = include_set.contains(key);
1145 if forced {
1146 emitted.push(section_md.clone());
1147 used += cost;
1148 remaining = remaining.saturating_sub(cost);
1149 } else if !overbudget && remaining >= cost {
1150 emitted.push(section_md.clone());
1151 used += cost;
1152 remaining -= cost;
1153 } else {
1154 hints.push(((*key).to_string(), cost));
1155 }
1156 }
1157
1158 let mode = if overbudget {
1159 "overbudget"
1160 } else if hints.is_empty() {
1161 "complete"
1162 } else {
1163 "reduced"
1164 };
1165
1166 let mut md = String::new();
1167 md.push_str("---\n");
1168 md.push_str(&format!("_report_mode: {mode}\n"));
1169 md.push_str(&format!("_budget_requested: {budget}\n"));
1170 md.push_str(&format!("_budget_used: {used}\n"));
1171 md.push_str("---\n\n");
1172 md.push_str(&hard);
1173 for section in &emitted {
1174 md.push_str(section);
1175 }
1176
1177 if !hints.is_empty() {
1178 md.push_str("## Hints\n\n");
1179 md.push_str(
1180 "_(heavy sections omitted under the token budget — re-query with the key)_\n\n",
1181 );
1182 for (key, tokens) in &hints {
1183 md.push_str(&format!("- `{key}` — estimated_tokens: {tokens}\n"));
1184 }
1185 md.push('\n');
1186 }
1187
1188 if !unknown_includes.is_empty() {
1189 md.push_str("## Warnings\n\n");
1190 for k in &unknown_includes {
1191 md.push_str(&format!(
1192 "- unknown include key `{k}` — allowed: {}\n",
1193 ALLOWED_REPORT_INCLUDE_KEYS.join(", ")
1194 ));
1195 }
1196 md.push('\n');
1197 }
1198
1199 RenderedFidelityReport {
1200 markdown: md,
1201 mode: mode.to_string(),
1202 hints,
1203 budget_used: used,
1204 }
1205}
1206
1207pub fn compute_fidelity_report(
1222 engine: &Engine,
1223 workspace_root: &Path,
1224 binding: &Binding,
1225 resolved: &ResolvedIngest,
1226 key: &FindingKey,
1227) -> FidelityReport {
1228 let binding_id = resolved.name.clone();
1229 let dest = resolved.destination_mem.clone();
1230
1231 let sync_state = engine
1233 .mem_config_for(&dest)
1234 .map(|c| c.sync_state.clone())
1235 .unwrap_or_default();
1236 let mut capabilities: Vec<FacetCapability> = Vec::new();
1237 let mut freshness: Vec<FacetFreshness> = Vec::new();
1238 let mut any_change_detectable = false;
1239 for source in &resolved.sources {
1240 let ResolvedSource::Primary(p) = source else {
1241 continue;
1242 };
1243 let caps = medium_capabilities(p.medium_type);
1244 let medium_type = serde_json::to_value(p.medium_type)
1245 .ok()
1246 .and_then(|v| v.as_str().map(str::to_string))
1247 .unwrap_or_default();
1248 let strategy = resolve_change_strategy(p, workspace_root);
1249 let signal = signal_wire(strategy).to_string();
1250 let signal_readable = match strategy {
1260 ChangeStrategy::Git => {
1261 super::resolve::find_git_root(&super::resolve::source_base_path(p, workspace_root))
1262 .is_some()
1263 }
1264 _ => true,
1265 };
1266 let change_detectable =
1267 caps.change_signal && strategy != ChangeStrategy::None && signal_readable;
1268 any_change_detectable |= change_detectable;
1269
1270 capabilities.push(FacetCapability::from_caps(
1271 p.name.clone(),
1272 medium_type,
1273 caps,
1274 strategy,
1275 ));
1276
1277 let synced = sync_state
1278 .get(&format!("{binding_id}/{}#synced", p.name))
1279 .cloned();
1280 let verified = sync_state
1281 .get(&format!("{binding_id}/{}#verified", p.name))
1282 .cloned();
1283 freshness.push(FacetFreshness {
1284 facet: p.name.clone(),
1285 signal,
1286 synced,
1287 verified,
1288 change_detectable,
1289 });
1290 }
1291
1292 let source_moved_past_synced = if any_change_detectable {
1293 Some(source_moved(engine, resolved, workspace_root))
1294 } else {
1295 None
1296 };
1297
1298 let mut s_d: Vec<String> = Vec::new();
1300 let mut enumerable_facets = 0usize;
1301 let mut empty_enumerable_facets: BTreeSet<String> = BTreeSet::new();
1308 let mut malformed_patterns: Vec<String> = Vec::new();
1313 let mut legacy_patterns: Vec<String> = Vec::new();
1314 let mut partiality_reasons: Vec<String> = Vec::new();
1320 for source in &resolved.sources {
1321 if let ResolvedSource::Primary(p) = source {
1322 let caps = medium_capabilities(p.medium_type);
1323 if caps.enumerable {
1324 enumerable_facets += 1;
1325 }
1326 let walked = enumerate_source_artifacts_reported(
1327 engine,
1328 p,
1329 &resolved.deny_paths,
1330 workspace_root,
1331 );
1332 if caps.enumerable && walked.files.is_empty() {
1333 empty_enumerable_facets.insert(p.name.clone());
1334 }
1335 for m in &walked.malformed {
1336 malformed_patterns.push(format!("`{}` in facet `{}`", m, p.name));
1337 }
1338 for note in &walked.legacy_dialect {
1339 legacy_patterns.push(format!("`{}` in facet `{}`", note.pattern, p.name));
1340 }
1341 if let Some(reason) = walked.partiality_reason() {
1342 partiality_reasons.push(format!("facet `{}`: {reason}", p.name));
1343 }
1344 s_d.extend(walked.files);
1345 }
1346 }
1347 s_d.sort();
1348 s_d.dedup();
1349
1350 let denominator = if !partiality_reasons.is_empty() {
1351 DenominatorBasis::Partial {
1354 count: s_d.len(),
1355 reason: partiality_reasons.join("; "),
1356 }
1357 } else if !s_d.is_empty() {
1358 DenominatorBasis::Enumerated { count: s_d.len() }
1359 } else if enumerable_facets == 0 {
1360 DenominatorBasis::NonEnumerable {
1361 reason: "the medium type(s) are not enumerable this cycle".to_string(),
1362 }
1363 } else if !legacy_patterns.is_empty() {
1364 DenominatorBasis::NonEnumerable {
1369 reason: format!(
1370 "scope pattern(s) still written against the workspace root rather than the \
1371 source pointer, so they select nothing under the pointer join: {}. Rewrite \
1372 them relative to the source's pointer",
1373 legacy_patterns.join(", ")
1374 ),
1375 }
1376 } else {
1377 DenominatorBasis::NonEnumerable {
1381 reason: "no source artifacts enumerated in scope".to_string(),
1382 }
1383 };
1384
1385 let mut direct_covered = 0usize;
1386 let mut tree_only_covered = 0usize;
1387 let mut uncovered: Vec<String> = Vec::new();
1388 let mut tree_fanout: BTreeMap<(String, String), usize> = BTreeMap::new();
1389 let entity_end_reconciled = engine.entity_set_is_reconcilable(dest.as_str()).is_ok();
1390 for file in &s_d {
1391 let refs = engine.anchors_referencing_artifact(file);
1405 let mine: Vec<&(crate::EntityId, crate::anchor::Anchor)> = refs
1406 .iter()
1407 .filter(|(eid, a)| {
1408 eid.mem() == dest.as_str()
1409 && a.binding
1410 .as_deref()
1411 .map(|b| b == key.binding_hash.as_str())
1412 .unwrap_or(true)
1413 && (!entity_end_reconciled || !engine.entity_is_absent(eid))
1414 })
1415 .collect();
1416 if mine.is_empty() {
1417 uncovered.push(file.clone());
1418 continue;
1419 }
1420 let has_non_tree = mine.iter().any(|(_, a)| a.grain != AnchorGrain::Tree);
1421 if has_non_tree {
1422 direct_covered += 1;
1423 } else {
1424 tree_only_covered += 1;
1425 }
1426 for (eid, a) in &mine {
1428 if a.grain == AnchorGrain::Tree {
1429 *tree_fanout
1430 .entry((eid.as_ref().to_string(), a.artifact.clone()))
1431 .or_insert(0) += 1;
1432 }
1433 }
1434 }
1435 let tree_anchors: Vec<TreeFanout> = tree_fanout
1436 .into_iter()
1437 .map(|((entity, artifact), fanout)| TreeFanout {
1438 entity,
1439 artifact,
1440 fanout,
1441 })
1442 .collect();
1443
1444 let coverage = GrainCoverage {
1445 denominator,
1446 direct_covered,
1447 tree_only_covered,
1448 uncovered: uncovered.clone(),
1449 tree_anchors,
1450 };
1451
1452 let population = crate::ingest::anchor_population::population_for(
1456 engine,
1457 resolved,
1458 Some(key.binding_hash.as_str()),
1459 );
1460 let mut anchors = AnchorComposition {
1461 counted_rows: population.included.len(),
1462 distinct_artifacts: population.distinct_artifacts(),
1463 excluded_other_binding: population
1464 .excluded_count(crate::ingest::anchor_population::ExclusionReason::OtherBinding),
1465 excluded_out_of_scope: population
1466 .excluded_count(crate::ingest::anchor_population::ExclusionReason::OutOfScope),
1467 excluded_artifacts: population
1468 .excluded
1469 .iter()
1470 .map(|e| format!("{} ({})", e.artifact, e.reason.as_wire()))
1471 .collect(),
1472 counted_without_provenance: population.without_provenance,
1473 dangling: population.dangling.len(),
1474 dangling_rows: population
1475 .dangling
1476 .iter()
1477 .map(|d| format!("{} → {}", d.entity, d.artifact))
1478 .collect(),
1479 unreconciled: population.unreconciled.map(str::to_string),
1480 span_unvalidated: population
1481 .included
1482 .iter()
1483 .filter(|(_, r)| r.anchor.span_unvalidated)
1484 .count(),
1485 hash_from_backfill: population
1486 .included
1487 .iter()
1488 .filter(|(_, r)| {
1489 r.anchor.hash_source == Some(crate::anchor::AnchorHashSource::Backfill)
1490 })
1491 .count(),
1492 ..Default::default()
1493 };
1494 for (_eid, resolved_anchor) in population.included {
1495 let a = &resolved_anchor.anchor;
1496 *anchors
1497 .by_class
1498 .entry(a.class.as_wire().to_string())
1499 .or_insert(0) += 1;
1500 *anchors
1501 .by_grain
1502 .entry(a.grain.as_wire().to_string())
1503 .or_insert(0) += 1;
1504 if a.class == AnchorProvenanceClass::Authored {
1505 anchors.authored += 1;
1506 continue; }
1508 match resolved_anchor.state {
1509 Some(AnchorState::Resolves) => {
1510 anchors.resolves += 1;
1511 anchors.observed += 1;
1512 }
1513 Some(AnchorState::Drifted) => {
1514 anchors.drifted += 1;
1515 anchors.observed += 1;
1516 }
1517 Some(AnchorState::Recheck) => {
1518 anchors.recheck += 1;
1519 anchors.observed += 1;
1520 }
1521 Some(AnchorState::Orphaned) => {
1522 anchors.orphaned += 1;
1523 anchors.observed += 1;
1524 }
1525 None => anchors.unobserved += 1,
1526 }
1527 }
1528
1529 let mut findings_by_class: BTreeMap<String, usize> = BTreeMap::new();
1531 let mut backlog = 0usize;
1532 let mut superseded: Vec<String> = Vec::new();
1533 if let Some((mem, name)) = binding_id.split_once('/')
1534 && let Ok(Some(store)) = read_findings_store(workspace_root, mem, name)
1535 {
1536 for f in store.current(key) {
1537 *findings_by_class
1538 .entry(f.class.as_wire().to_string())
1539 .or_insert(0) += 1;
1540 if f.class == FindingClass::QueuedForAdjudication {
1541 backlog += 1;
1542 }
1543 }
1544 for f in store.superseded(key) {
1545 superseded.push(format!(
1546 "[{}] {} ({})",
1547 f.class.as_wire(),
1548 finding_target_label(&f.target),
1549 f.facet
1550 ));
1551 }
1552 }
1553
1554 let mut disposed_excluded_rationales: Vec<(String, String)> = Vec::new();
1560 if let Some((mem, name)) = binding_id.split_once('/')
1561 && let Ok(Some(state)) = read_advance_store(workspace_root, mem, name)
1562 {
1563 let uncovered_set: std::collections::BTreeSet<&str> =
1564 uncovered.iter().map(String::as_str).collect();
1565 for (artifact, rationale) in &state.exclusions {
1566 if uncovered_set.contains(artifact.as_str()) {
1567 disposed_excluded_rationales.push((artifact.clone(), rationale.clone()));
1568 }
1569 }
1570 }
1571 let disposed_excluded = disposed_excluded_rationales.len();
1572
1573 let mut degradations: Vec<String> = Vec::new();
1575 for c in &capabilities {
1576 if !c.change_signal || c.signal == "none" {
1577 degradations.push(format!(
1578 "change-signal-none:`{}` — freshness is unknowable for this facet",
1579 c.facet
1580 ));
1581 }
1582 if !c.enumerable {
1583 degradations.push(format!(
1584 "enumeration-unavailable:`{}` — `S(D)` coverage denominator not computable",
1585 c.facet
1586 ));
1587 } else if empty_enumerable_facets.contains(&c.facet) {
1588 degradations.push(format!(
1596 "enumeration-empty:`{}` — the medium claims enumerability but the walk yielded \
1597 no artifacts; coverage is reported over anchors only",
1598 c.facet
1599 ));
1600 }
1601 if !c.base_version_retrievable {
1602 degradations.push(format!(
1603 "base-version-unretrievable:`{}` — prune degrades to conflict-flagging",
1604 c.facet
1605 ));
1606 }
1607 }
1608 if anchors.recheck > 0 {
1609 degradations.push(format!(
1610 "hash-adjudication-deferred — {} anchor(s) recheck (unstable medium / hash \
1611 unavailable), not asserted drift",
1612 anchors.recheck
1613 ));
1614 }
1615 if anchors.unobserved > 0 {
1616 degradations.push(format!(
1617 "anchors-unobserved — {} anchor(s) could not be observed this pass",
1618 anchors.unobserved
1619 ));
1620 }
1621
1622 let adopt = super::render::mem_predates_binding(engine, resolved);
1626 let effective_coverage = crate::binding::effective_coverage_semantics(binding);
1627
1628 FidelityReport {
1629 legacy_dialect_patterns: legacy_patterns,
1630 binding: binding_id,
1631 destination_mem: dest,
1632 adopt,
1633 coverage_semantics: effective_coverage.value,
1634 coverage_semantics_declared: effective_coverage.declared,
1635 capabilities,
1636 freshness,
1637 source_moved_past_synced,
1638 coverage,
1639 anchors,
1640 findings_by_class,
1641 backlog,
1642 superseded,
1643 disposed_excluded,
1644 disposed_excluded_rationales,
1645 degradations,
1646 }
1647}
1648
1649fn strategy_retrieves_base(strategy: ChangeStrategy) -> bool {
1658 matches!(strategy, ChangeStrategy::Git | ChangeStrategy::Graph)
1659}
1660
1661fn signal_wire(strategy: ChangeStrategy) -> &'static str {
1664 match strategy {
1665 ChangeStrategy::None => "none",
1666 ChangeStrategy::Git => "git",
1667 ChangeStrategy::Mtime => "mtime",
1668 ChangeStrategy::Graph => "graph",
1669 }
1670}
1671
1672fn finding_target_label(target: &super::findings::FindingTarget) -> String {
1674 match target {
1675 super::findings::FindingTarget::Anchor { entity, artifact } => {
1676 format!("{entity} → {artifact}")
1677 }
1678 super::findings::FindingTarget::Artifact { artifact } => artifact.clone(),
1679 }
1680}
1681
1682#[cfg(test)]
1683mod tests {
1684 use super::*;
1685
1686 fn base_report() -> FidelityReport {
1689 FidelityReport {
1690 legacy_dialect_patterns: Vec::new(),
1691 binding: "engine/graph".to_string(),
1692 destination_mem: "engine".to_string(),
1693 adopt: false,
1694 coverage_semantics: CoverageSemantics::Exhaustive,
1695 coverage_semantics_declared: true,
1696 capabilities: vec![FacetCapability {
1697 facet: "src".to_string(),
1698 medium_type: "codebase".to_string(),
1699 enumerable: true,
1700 change_signal: true,
1701 base_version_retrievable: true,
1702 anchor_namespace: "path".to_string(),
1703 signal: "git".to_string(),
1704 }],
1705 freshness: vec![FacetFreshness {
1706 facet: "src".to_string(),
1707 signal: "git".to_string(),
1708 synced: Some("deadbeef".to_string()),
1709 verified: None,
1710 change_detectable: true,
1711 }],
1712 source_moved_past_synced: Some(false),
1713 coverage: GrainCoverage {
1714 denominator: DenominatorBasis::Enumerated { count: 10 },
1715 direct_covered: 6,
1716 tree_only_covered: 3,
1717 uncovered: vec!["src/a.rs".to_string()],
1718 tree_anchors: vec![TreeFanout {
1719 entity: "engine--big".to_string(),
1720 artifact: "src/".to_string(),
1721 fanout: 3,
1722 }],
1723 },
1724 anchors: AnchorComposition {
1725 by_class: BTreeMap::from([
1726 ("anchored".to_string(), 5),
1727 ("authored".to_string(), 2),
1728 ]),
1729 by_grain: BTreeMap::from([("file".to_string(), 4), ("tree".to_string(), 1)]),
1730 authored: 2,
1731 observed: 5,
1732 resolves: 4,
1733 drifted: 0,
1734 recheck: 1,
1735 orphaned: 0,
1736 unobserved: 0,
1737 ..Default::default()
1738 },
1739 findings_by_class: BTreeMap::from([
1740 ("uncovered".to_string(), 1),
1741 ("queued-for-adjudication".to_string(), 1),
1742 ]),
1743 backlog: 1,
1744 superseded: Vec::new(),
1745 disposed_excluded: 0,
1746 disposed_excluded_rationales: Vec::new(),
1747 degradations: vec!["hash-adjudication-deferred — 1 anchor(s) recheck".to_string()],
1748 }
1749 }
1750
1751 #[test]
1756 fn b1_renders_all_elements_deterministically() {
1757 let r = base_report();
1758 let a = render_fidelity_report(&r, 8_000, &[]);
1759 let b = render_fidelity_report(&r, 8_000, &[]);
1760 assert_eq!(a.markdown, b.markdown, "deterministic — identical bytes");
1761
1762 let md = &a.markdown;
1763 assert!(md.contains("direct-covered (file / span anchors): 6/10"));
1765 assert!(md.contains(
1766 "tree-anchor fan-out (separate axis): 1 tree anchor(s) fanning out over 3 file(s)"
1767 ));
1768 assert!(
1770 !md.contains("9/10"),
1771 "tree fan-out must not blend into direct coverage"
1772 );
1773 assert!(md.contains("anchor-resolution %:** 4/5"));
1775 assert!(md.contains("`authored` bucket (excluded from coverage/accuracy denominators): 2"));
1777 assert!(md.contains("tier-3 adjudication backlog:** 1"));
1779 assert!(md.contains("## Capability matrix"));
1781 assert!(md.contains("## Degradations"));
1782 assert!(md.contains("hash-adjudication-deferred"));
1783 assert!(md.contains("per-medium enumeration `S(D)` = **10**"));
1785 }
1786
1787 #[test]
1790 fn b2_detectionless_medium_freshness_unknowable_never_green() {
1791 let mut r = base_report();
1792 r.capabilities = vec![FacetCapability {
1793 facet: "manual".to_string(),
1794 medium_type: "web".to_string(),
1795 enumerable: false,
1796 change_signal: false,
1797 base_version_retrievable: false,
1798 anchor_namespace: "url".to_string(),
1799 signal: "none".to_string(),
1800 }];
1801 r.freshness = vec![FacetFreshness {
1802 facet: "manual".to_string(),
1803 signal: "none".to_string(),
1804 synced: Some("should-never-render-green".to_string()),
1807 verified: Some("nor-this".to_string()),
1808 change_detectable: false,
1809 }];
1810 r.source_moved_past_synced = None;
1811 let out = render_fidelity_report(&r, 8_000, &[]);
1812 let md = &out.markdown;
1813 assert!(md.contains("signal: `none`"));
1814 assert!(md.contains("freshness unknowable"));
1815 assert!(!md.contains("should-never-render-green"));
1818 assert!(
1819 !md.contains("`#synced`: `"),
1820 "no synced token rendered for a non-detectable medium"
1821 );
1822 assert!(
1823 !md.contains("at its `#synced` baseline"),
1824 "no green 'at baseline' verdict"
1825 );
1826 }
1827
1828 #[test]
1836 fn b1_base_retrievability_follows_resolved_strategy_not_medium_ceiling() {
1837 use crate::pipeline::MediumType;
1838
1839 assert!(medium_capabilities(MediumType::Filesystem).base_version_retrievable);
1841
1842 let fs_mtime = FacetCapability::from_caps(
1844 "prose".to_string(),
1845 "filesystem".to_string(),
1846 medium_capabilities(MediumType::Filesystem),
1847 ChangeStrategy::Mtime,
1848 );
1849 assert!(
1850 !fs_mtime.base_version_retrievable,
1851 "filesystem+mtime has no retrievable base leg — degrades to conflict-flag"
1852 );
1853 assert_eq!(fs_mtime.signal, "mtime");
1854
1855 let fs_git = FacetCapability::from_caps(
1856 "prose".to_string(),
1857 "filesystem".to_string(),
1858 medium_capabilities(MediumType::Filesystem),
1859 ChangeStrategy::Git,
1860 );
1861 assert!(
1862 fs_git.base_version_retrievable,
1863 "filesystem backed by git keeps the never-clobber base leg"
1864 );
1865
1866 assert!(!strategy_retrieves_base(ChangeStrategy::None));
1868 assert!(!strategy_retrieves_base(ChangeStrategy::Mtime));
1869 assert!(strategy_retrieves_base(ChangeStrategy::Git));
1870 assert!(strategy_retrieves_base(ChangeStrategy::Graph));
1871
1872 let mut r = base_report();
1876 r.capabilities = vec![fs_mtime.clone()];
1877 r.degradations = if !fs_mtime.base_version_retrievable {
1878 vec![format!(
1879 "base-version-unretrievable:`{}` — prune degrades to conflict-flagging",
1880 fs_mtime.facet
1881 )]
1882 } else {
1883 Vec::new()
1884 };
1885 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
1886 assert!(
1887 md.contains("base-version-unretrievable:`prose` — prune degrades to conflict-flagging"),
1888 "filesystem+mtime surfaces the conflict-flag degradation in the report"
1889 );
1890 }
1891
1892 #[test]
1895 fn b3_aggregates_always_ship_at_zero_budget() {
1896 let r = base_report();
1897 let out = render_fidelity_report(&r, 0, &[]);
1898 assert_eq!(out.mode, "overbudget");
1899 let md = &out.markdown;
1900 assert!(md.contains("direct-covered (file / span anchors): 6/10"));
1902 assert!(md.contains("tier-3 adjudication backlog:** 1"));
1903 assert!(md.contains("## Capability matrix"));
1904 assert!(!md.contains("## Uncovered artifacts"));
1906 assert!(md.contains("## Hints"));
1907 assert!(out.hints.iter().any(|(k, _)| k == "uncovered_artifacts"));
1908 }
1909
1910 #[test]
1914 fn b3_large_facet_list_truncates_then_include_forces() {
1915 let mut r = base_report();
1916 r.coverage.uncovered = (0..500).map(|i| format!("src/file_{i}.rs")).collect();
1918 let hard_cost = estimate_tokens(&render_hard_required(&r));
1920 let out = render_fidelity_report(&r, hard_cost + 5, &[]);
1921 assert_eq!(out.mode, "reduced");
1922 assert!(
1923 !out.markdown.contains("src/file_499.rs"),
1924 "big list not rendered unbounded"
1925 );
1926 assert!(out.markdown.contains("## Hints"));
1927 let (_, est) = out
1928 .hints
1929 .iter()
1930 .find(|(k, _)| k == "uncovered_artifacts")
1931 .expect("uncovered list hinted");
1932 assert!(*est > 5, "the hint carries a real estimated_tokens figure");
1933
1934 let forced =
1936 render_fidelity_report(&r, hard_cost + 5, &["uncovered_artifacts".to_string()]);
1937 assert!(
1938 forced.markdown.contains("src/file_499.rs"),
1939 "include forces the full list"
1940 );
1941 }
1942
1943 #[test]
1946 fn b4_curated_vs_exhaustive_framing() {
1947 let mut exhaustive = base_report();
1948 exhaustive.coverage_semantics = CoverageSemantics::Exhaustive;
1949 let ex_md = render_fidelity_report(&exhaustive, 8_000, &[]).markdown;
1950 assert!(ex_md.contains("Exhaustive coverage:"));
1951 assert!(ex_md.contains("are **findings**"));
1952
1953 let mut curated = base_report();
1954 curated.coverage_semantics = CoverageSemantics::Curated;
1955 let cur_md = render_fidelity_report(&curated, 8_000, &[]).markdown;
1956 assert!(cur_md.contains("Curated coverage:"));
1957 assert!(cur_md.contains("**information**"));
1958 assert!(
1959 !cur_md.contains("are **findings**"),
1960 "curated never frames unaccounted as findings"
1961 );
1962 }
1963
1964 #[test]
1967 fn b4_disposition_excludes_from_exhaustive_findings() {
1968 let mut r = base_report();
1969 r.coverage_semantics = CoverageSemantics::Exhaustive;
1970 r.coverage.uncovered = vec!["src/a.rs".to_string(), "src/b.rs".to_string()];
1971 r.disposed_excluded = 1;
1972 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
1973 assert!(md.contains("1 unaccounted artifact(s)"));
1975 assert!(md.contains("(1 disposed excluded)"));
1976 }
1977
1978 #[test]
1981 fn b4_authored_exclusion_rationale_is_rendered() {
1982 let mut r = base_report();
1983 r.coverage_semantics = CoverageSemantics::Exhaustive;
1984 r.coverage.uncovered = vec!["src/gen.rs".to_string()];
1985 r.disposed_excluded = 1;
1986 r.disposed_excluded_rationales =
1987 vec![("src/gen.rs".to_string(), "generated; no entity".to_string())];
1988 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
1989 assert!(md.contains("Excluded on purpose (persisted dispositions):"));
1990 assert!(md.contains("`src/gen.rs` — generated; no entity"));
1991 }
1992
1993 #[test]
1996 fn b5_denominator_provenance_stated() {
1997 let r = base_report();
1998 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
1999 assert!(md.contains("## Denominator provenance"));
2000 assert!(md.contains("per-medium enumeration `S(D)` = **10**"));
2001
2002 let mut non = base_report();
2003 non.coverage.denominator = DenominatorBasis::NonEnumerable {
2004 reason: "the medium type(s) are not enumerable this cycle".to_string(),
2005 };
2006 let md2 = render_fidelity_report(&non, 8_000, &[]).markdown;
2007 assert!(md2.contains("No `S(D)` denominator"));
2008 assert!(md2.contains("not enumerable this cycle"));
2009 }
2010
2011 #[test]
2017 fn e1_adopt_report_renders_onboarding_no_red_verdict() {
2018 let mut r = base_report();
2019 r.adopt = true;
2020 r.coverage_semantics = CoverageSemantics::Exhaustive;
2021 r.coverage.uncovered = (0..5).map(|i| format!("src/file_{i}.rs")).collect();
2022 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
2023
2024 assert!(md.contains("## Adopting — first verify"));
2026 assert!(md.contains("0% anchored is expected — this is onboarding, not a failure."));
2027 assert!(
2029 md.contains("**Backfill path:** run `memstead projection brief engine/graph --sync`")
2030 );
2031 assert!(
2034 !md.contains("are **findings**"),
2035 "pre-binding history must not produce a red findings verdict"
2036 );
2037 assert!(md.contains("Exhaustive coverage (onboarding):"));
2038 assert!(md.contains("backfill worklist"));
2039
2040 r.adopt = false;
2042 let md2 = render_fidelity_report(&r, 8_000, &[]).markdown;
2043 assert!(!md2.contains("## Adopting — first verify"));
2044 assert!(md2.contains("are **findings**"));
2045 }
2046
2047 #[test]
2049 fn unknown_include_key_warns() {
2050 let r = base_report();
2051 let out = render_fidelity_report(&r, 8_000, &["bogus".to_string()]);
2052 assert!(out.markdown.contains("unknown include key `bogus`"));
2053 }
2054
2055 use crate::anchor::{Anchor, AnchorHashStability, AnchorProvenanceClass, AnchorSidecar};
2058 use crate::binding::{
2059 BINDING_VERSION, Binding, BuildMode, BuildOperation, DEFAULT_ADJUDICATION_CAP,
2060 DEFAULT_FULL_RESYNC_EVERY, Operations, VerifyOperation,
2061 };
2062 use crate::ingest::findings::verify_binding;
2063 use crate::ingest::resolve::resolve_binding_run;
2064 use crate::pipeline::{IngestTrigger, MediumType, PatternEntry, PatternMode};
2065 use crate::pipeline_store::{load_pipeline_configs, write_binding};
2066 use crate::workspace::{
2067 Mount, MountCapability, MountLifecycle, MountStorage, Workspace, WorkspaceSettings,
2068 };
2069 use crate::workspace_store::WorkspaceStoreAdapter;
2070
2071 #[test]
2078 fn compute_report_end_to_end() {
2079 let tmp = tempfile::tempdir().unwrap();
2080 let (report, outcome, md) = end_to_end_report(tmp.path(), &["direct", "tree", "auth"]);
2081 end_to_end_body(&report, &outcome, &md);
2082 }
2083
2084 #[test]
2089 fn coverage_does_not_rest_on_an_anchor_whose_entity_is_gone() {
2090 let tmp = tempfile::tempdir().unwrap();
2091 let (report, _outcome, md) = end_to_end_report(tmp.path(), &["tree"]);
2092 assert_eq!(
2093 report.coverage.direct_covered, 0,
2094 "the only direct anchor on present.rs is dangling, so nothing covers it directly"
2095 );
2096 assert!(
2097 report
2098 .coverage
2099 .uncovered
2100 .contains(&"src/present.rs".to_string()),
2101 "and the artifact reads uncovered rather than covered by a phantom"
2102 );
2103 assert_eq!(report.anchors.dangling, 2);
2105 assert_eq!(report.anchors.counted_rows, 1);
2106 assert_eq!(report.anchors.unreconciled, None);
2107 assert!(
2108 md.contains("name an entity this mem no longer holds"),
2109 "and the report says so on the page, not only in the struct"
2110 );
2111 }
2112
2113 fn end_to_end_report(
2119 root: &std::path::Path,
2120 entity_slugs: &[&str],
2121 ) -> (
2122 FidelityReport,
2123 crate::ingest::findings::VerifyOutcome,
2124 String,
2125 ) {
2126 let mem_dir = root.join("mem");
2127 std::fs::create_dir_all(mem_dir.join(".memstead")).unwrap();
2128 std::fs::write(
2129 mem_dir.join(".memstead").join("config.json"),
2130 r#"{"format":1,"schema":"default@1.0.0","version":"1.0.0"}"#,
2131 )
2132 .unwrap();
2133
2134 std::fs::create_dir_all(root.join(".memstead")).unwrap();
2135 std::fs::write(
2136 root.join(".memstead").join("workspace.toml"),
2137 "format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
2138 )
2139 .unwrap();
2140 let mount = Mount {
2141 mem: "engine".to_string(),
2142 schema: Some("default@1.0.0".parse().unwrap()),
2143 storage: MountStorage::Folder {
2144 path: mem_dir.clone(),
2145 },
2146 capability: MountCapability::Write,
2147 lifecycle: MountLifecycle::Eager,
2148 cross_linkable: false,
2149 migration_target: None,
2150 };
2151 crate::FileWorkspaceStore::new()
2152 .save_state(
2153 root,
2154 &Workspace {
2155 mounts: vec![mount],
2156 settings: WorkspaceSettings::default(),
2157 },
2158 )
2159 .unwrap();
2160
2161 let out = std::process::Command::new("git")
2162 .args(["init", "-q"])
2163 .current_dir(root)
2164 .output()
2165 .unwrap();
2166 assert!(out.status.success());
2167 std::fs::create_dir_all(root.join("src").join("sub")).unwrap();
2168 std::fs::write(root.join("src").join("present.rs"), "fn a() {}\n").unwrap();
2169 std::fs::write(root.join("src").join("uncovered.rs"), "fn b() {}\n").unwrap();
2170 std::fs::write(root.join("src").join("sub").join("deep.rs"), "fn c() {}\n").unwrap();
2171
2172 let mk = |artifact: &str, grain: AnchorGrain, class: AnchorProvenanceClass| Anchor {
2173 artifact: artifact.to_string(),
2174 grain,
2175 class,
2176 at_version: None,
2177 hash: class.is_hash_bearing().then(|| "recorded".to_string()),
2178 hash_stability: AnchorHashStability::Stable,
2179 derived_from: Vec::new(),
2180 binding: None,
2181 source: None,
2182 span_unvalidated: false,
2183 hash_source: None,
2184 };
2185 for slug in entity_slugs {
2189 std::fs::write(
2190 mem_dir.join(format!("{slug}.md")),
2191 "---\ntype: decision\n---\n\n# E\n\n## Decision\n\nBody.\n",
2192 )
2193 .unwrap();
2194 }
2195 let mut sidecar = AnchorSidecar::default();
2196 sidecar.set(
2197 "engine--direct",
2198 vec![mk(
2199 "src/present.rs",
2200 AnchorGrain::File,
2201 AnchorProvenanceClass::Anchored,
2202 )],
2203 );
2204 sidecar.set(
2205 "engine--tree",
2206 vec![mk(
2207 "src/sub/",
2208 AnchorGrain::Tree,
2209 AnchorProvenanceClass::Anchored,
2210 )],
2211 );
2212 sidecar.set(
2214 "engine--auth",
2215 vec![mk(
2216 "src/present.rs",
2217 AnchorGrain::File,
2218 AnchorProvenanceClass::Authored,
2219 )],
2220 );
2221 std::fs::write(
2222 mem_dir.join(crate::anchor::ANCHOR_SIDECAR_PATH),
2223 sidecar.to_bytes(),
2224 )
2225 .unwrap();
2226
2227 write_binding(
2228 root,
2229 "engine",
2230 "graph",
2231 &Binding {
2232 version: BINDING_VERSION,
2233 intent: None,
2234 sources: vec![crate::pipeline::Source {
2235 name: "graph".to_string(),
2236 medium_type: MediumType::Codebase,
2237 pointer: String::new(),
2238 change_detection: Some("git".to_string()),
2239 scope: vec![PatternEntry {
2240 path: "src/**/*.rs".to_string(),
2241 mode: PatternMode::Allow,
2242 }],
2243 engagement: None,
2244 preparation: None,
2245 }],
2246 reference_mems: Vec::new(),
2247 destination_mem: "engine".to_string(),
2248 deny_paths: Vec::new(),
2249 coverage_semantics: None,
2250 rules: None,
2251 prune: None,
2252 operations: Operations {
2253 build: Some(BuildOperation {
2254 mode: BuildMode::Discovery,
2255 trigger: IngestTrigger::Loop,
2256 batch_size: 20,
2257 post_actions: None,
2258 }),
2259 sync: None,
2260 verify: Some(VerifyOperation {
2261 trigger: IngestTrigger::Manual,
2262 batch_size: 20,
2263 adjudication_cap: DEFAULT_ADJUDICATION_CAP,
2264 full_resync_every: DEFAULT_FULL_RESYNC_EVERY,
2265 }),
2266 },
2267 },
2268 )
2269 .unwrap();
2270
2271 let engine = Engine::from_workspace_root(root).unwrap();
2272 let configs = load_pipeline_configs(root).unwrap();
2273 let binding = &configs.bindings[0].config;
2274 let resolved = resolve_binding_run("engine/graph", binding).unwrap();
2275
2276 let outcome = verify_binding(&engine, root, binding, &resolved).unwrap();
2278
2279 let report = compute_fidelity_report(&engine, root, binding, &resolved, &outcome.key);
2281 let md = render_fidelity_report(&report, 8_000, &[]).markdown;
2282 (report, outcome, md)
2283 }
2284
2285 fn end_to_end_body(
2286 report: &FidelityReport,
2287 outcome: &crate::ingest::findings::VerifyOutcome,
2288 md: &str,
2289 ) {
2290 assert_eq!(
2292 report.coverage.denominator,
2293 DenominatorBasis::Enumerated { count: 3 }
2294 );
2295 assert_eq!(report.coverage.direct_covered, 1);
2298 assert_eq!(report.coverage.tree_only_covered, 1);
2299 assert_eq!(
2300 report.coverage.uncovered,
2301 vec!["src/uncovered.rs".to_string()]
2302 );
2303 assert_eq!(report.coverage.tree_anchors.len(), 1);
2305 assert_eq!(report.coverage.tree_anchors[0].fanout, 1);
2306 assert_eq!(report.coverage.tree_anchors[0].artifact, "src/sub/");
2307 assert_eq!(report.anchors.authored, 1);
2309 assert_eq!(report.anchors.by_class.get("authored"), Some(&1));
2310 assert_eq!(report.anchors.observed, 2);
2315 assert_eq!(report.anchors.recheck, 1);
2316 assert_eq!(report.anchors.drifted, 1);
2317 assert_eq!(report.backlog, outcome.backlog);
2319 assert!(
2321 report
2322 .degradations
2323 .iter()
2324 .any(|d| d.contains("hash-adjudication-deferred"))
2325 );
2326 assert!(md.contains("per-medium enumeration `S(D)` = **3**"));
2328 assert!(!report.adopt);
2331 assert!(!md.contains("## Adopting — first verify"));
2332 }
2333
2334 #[test]
2339 fn compute_report_adopt_when_mem_predates_binding() {
2340 let tmp = tempfile::tempdir().unwrap();
2341 let root = tmp.path();
2342 let mem_dir = root.join("mem");
2343 std::fs::create_dir_all(mem_dir.join(".memstead")).unwrap();
2344 std::fs::write(
2345 mem_dir.join(".memstead").join("config.json"),
2346 r#"{"format":1,"schema":"default@1.0.0","version":"1.0.0"}"#,
2347 )
2348 .unwrap();
2349 std::fs::create_dir_all(root.join(".memstead")).unwrap();
2350 std::fs::write(
2351 root.join(".memstead").join("workspace.toml"),
2352 "format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
2353 )
2354 .unwrap();
2355 let mount = Mount {
2356 mem: "engine".to_string(),
2357 schema: Some("default@1.0.0".parse().unwrap()),
2358 storage: MountStorage::Folder {
2359 path: mem_dir.clone(),
2360 },
2361 capability: MountCapability::Write,
2362 lifecycle: MountLifecycle::Eager,
2363 cross_linkable: false,
2364 migration_target: None,
2365 };
2366 crate::FileWorkspaceStore::new()
2367 .save_state(
2368 root,
2369 &Workspace {
2370 mounts: vec![mount],
2371 settings: WorkspaceSettings::default(),
2372 },
2373 )
2374 .unwrap();
2375 let out = std::process::Command::new("git")
2376 .args(["init", "-q"])
2377 .current_dir(root)
2378 .output()
2379 .unwrap();
2380 assert!(out.status.success());
2381 std::fs::create_dir_all(root.join("src")).unwrap();
2382 std::fs::write(root.join("src").join("a.rs"), "fn a() {}\n").unwrap();
2384 std::fs::write(root.join("src").join("b.rs"), "fn b() {}\n").unwrap();
2385
2386 write_binding(
2387 root,
2388 "engine",
2389 "graph",
2390 &Binding {
2391 version: BINDING_VERSION,
2392 intent: None,
2393 sources: vec![crate::pipeline::Source {
2394 name: "graph".to_string(),
2395 medium_type: MediumType::Codebase,
2396 pointer: String::new(),
2397 change_detection: Some("git".to_string()),
2398 scope: vec![PatternEntry {
2399 path: "src/**/*.rs".to_string(),
2400 mode: PatternMode::Allow,
2401 }],
2402 engagement: None,
2403 preparation: None,
2404 }],
2405 reference_mems: Vec::new(),
2406 destination_mem: "engine".to_string(),
2407 deny_paths: Vec::new(),
2408 coverage_semantics: None,
2409 rules: None,
2410 prune: None,
2411 operations: Operations {
2412 build: Some(BuildOperation {
2413 mode: BuildMode::Discovery,
2414 trigger: IngestTrigger::Loop,
2415 batch_size: 20,
2416 post_actions: None,
2417 }),
2418 sync: None,
2419 verify: Some(VerifyOperation {
2420 trigger: IngestTrigger::Manual,
2421 batch_size: 20,
2422 adjudication_cap: DEFAULT_ADJUDICATION_CAP,
2423 full_resync_every: DEFAULT_FULL_RESYNC_EVERY,
2424 }),
2425 },
2426 },
2427 )
2428 .unwrap();
2429
2430 let engine = Engine::from_workspace_root(root).unwrap();
2431 let configs = load_pipeline_configs(root).unwrap();
2432 let binding = &configs.bindings[0].config;
2433 let resolved = resolve_binding_run("engine/graph", binding).unwrap();
2434 let outcome = verify_binding(&engine, root, binding, &resolved).unwrap();
2435 let report = compute_fidelity_report(&engine, root, binding, &resolved, &outcome.key);
2436
2437 assert!(
2439 report.adopt,
2440 "a no-anchor, never-synced mem predates its binding"
2441 );
2442 let md = render_fidelity_report(&report, 8_000, &[]).markdown;
2443 assert!(md.contains("## Adopting — first verify"));
2444 assert!(md.contains("0% anchored is expected"));
2445 assert!(!md.contains("are **findings**"));
2447 assert!(md.contains("Exhaustive coverage (onboarding):"));
2448 }
2449
2450 #[test]
2454 fn report_marks_resolved_coverage_semantics() {
2455 let mut resolved = base_report();
2456 resolved.coverage_semantics = CoverageSemantics::Curated;
2457 resolved.coverage_semantics_declared = false;
2458 let md = render_hard_required(&resolved);
2459 assert!(
2460 md.contains("curated (resolved from the sources' media — not declared)"),
2461 "resolved value carries the marker: {md}"
2462 );
2463
2464 let declared = base_report(); let md = render_hard_required(&declared);
2466 assert!(
2467 md.contains("**Coverage semantics:** exhaustive\n"),
2468 "declared value renders bare: {md}"
2469 );
2470 assert!(
2471 !md.contains("(resolved from the sources' media"),
2472 "no resolution marker on a declared value: {md}"
2473 );
2474 }
2475}
2476
2477#[cfg(test)]
2478mod rollup_tests {
2479 use super::*;
2480
2481 fn clean_report() -> FidelityReport {
2485 FidelityReport {
2486 legacy_dialect_patterns: Vec::new(),
2487 binding: "engine/graph".to_string(),
2488 destination_mem: "engine".to_string(),
2489 adopt: false,
2490 coverage_semantics: CoverageSemantics::Exhaustive,
2491 coverage_semantics_declared: true,
2492 capabilities: vec![FacetCapability {
2493 facet: "src".to_string(),
2494 medium_type: "codebase".to_string(),
2495 enumerable: true,
2496 change_signal: true,
2497 base_version_retrievable: true,
2498 anchor_namespace: "path".to_string(),
2499 signal: "git".to_string(),
2500 }],
2501 freshness: vec![FacetFreshness {
2502 facet: "src".to_string(),
2503 signal: "git".to_string(),
2504 synced: Some("deadbeef".to_string()),
2505 verified: None,
2506 change_detectable: true,
2507 }],
2508 source_moved_past_synced: Some(false),
2509 coverage: GrainCoverage {
2510 denominator: DenominatorBasis::Enumerated { count: 4 },
2511 direct_covered: 4,
2512 tree_only_covered: 0,
2513 uncovered: Vec::new(),
2514 tree_anchors: Vec::new(),
2515 },
2516 anchors: AnchorComposition {
2517 by_class: BTreeMap::from([("anchored".to_string(), 4)]),
2518 by_grain: BTreeMap::from([("file".to_string(), 4)]),
2519 authored: 0,
2520 observed: 4,
2521 resolves: 4,
2522 drifted: 0,
2523 recheck: 0,
2524 orphaned: 0,
2525 unobserved: 0,
2526 ..Default::default()
2527 },
2528 findings_by_class: BTreeMap::new(),
2529 backlog: 0,
2530 superseded: Vec::new(),
2531 disposed_excluded: 0,
2532 disposed_excluded_rationales: Vec::new(),
2533 degradations: Vec::new(),
2534 }
2535 }
2536
2537 #[test]
2543 fn unadjudicated_rows_block_clean_but_exclusions_do_not() {
2544 let mut r = clean_report();
2545 assert_eq!(
2546 r.rollup().verdict,
2547 RollupVerdict::Clean,
2548 "the baseline is clean"
2549 );
2550
2551 r.anchors.excluded_out_of_scope = 3;
2553 r.anchors.excluded_other_binding = 2;
2554 r.anchors.excluded_artifacts = vec!["src/a.rs (out-of-scope)".into()];
2555 assert_eq!(
2556 r.rollup().verdict,
2557 RollupVerdict::Clean,
2558 "excluding a row this binding does not answer for is an ANSWER, not a blind spot"
2559 );
2560
2561 let mut unobserved = r.clone();
2563 unobserved.anchors.unobserved = 1;
2564 let roll = unobserved.rollup();
2565 assert_eq!(roll.verdict, RollupVerdict::Inconclusive);
2566 assert!(
2567 roll.blind_spots
2568 .iter()
2569 .any(|b| b.contains("could not be observed")),
2570 "and it names itself: {:?}",
2571 roll.blind_spots
2572 );
2573
2574 let mut span = r.clone();
2576 span.anchors.span_unvalidated = 2;
2577 assert_eq!(span.rollup().verdict, RollupVerdict::Inconclusive);
2578
2579 let mut ent = r.clone();
2581 ent.anchors.unreconciled = Some("the mem's lazy entity load has not run".into());
2582 assert_eq!(ent.rollup().verdict, RollupVerdict::Inconclusive);
2583 }
2584
2585 #[test]
2590 fn all_five_conditions_are_reachable_in_the_report() {
2591 let mut r = clean_report();
2592 r.anchors.excluded_out_of_scope = 1;
2593 r.anchors.excluded_other_binding = 1;
2594 r.anchors.excluded_artifacts = vec![
2595 "src/a.rs (out-of-scope)".into(),
2596 "src/b.rs (other-binding)".into(),
2597 ];
2598 r.anchors.dangling = 1;
2599 r.anchors.dangling_rows = vec!["engine--gone → src/c.rs".into()];
2600 r.anchors.span_unvalidated = 1;
2601 r.anchors.hash_from_backfill = 1;
2602
2603 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
2604 for (needle, condition) in [
2605 ("outside this binding's declared scope", "scope-excluded"),
2606 ("written by another binding", "other-binding"),
2607 ("no longer holds", "dangling entity"),
2608 ("never checked against their artifact", "span not validated"),
2609 ("inferred by backfill", "baseline established by backfill"),
2610 ] {
2611 assert!(
2612 md.contains(needle),
2613 "{condition} is not reachable in the report; looked for {needle:?} in:\n{md}"
2614 );
2615 }
2616 }
2617
2618 #[test]
2620 fn clean_requires_a_substantive_pass_and_no_findings() {
2621 let mut r = clean_report();
2622 assert_eq!(r.rollup().verdict, RollupVerdict::Clean);
2623 assert!(r.rollup().blind_spots.is_empty());
2624 assert!(r.rollup().actions.is_empty());
2625
2626 r.findings_by_class.insert("drifted".to_string(), 2);
2627 let roll = r.rollup();
2628 assert_eq!(roll.verdict, RollupVerdict::Drifted);
2629 assert_eq!(roll.findings_total, 2);
2630 assert!(
2631 roll.actions[0].contains("moved since the entity was written"),
2632 "the top action is the concrete next step: {:?}",
2633 roll.actions
2634 );
2635 }
2636
2637 #[test]
2642 fn a_vacuous_zero_over_zero_is_inconclusive_not_clean() {
2643 let mut r = clean_report();
2644 r.coverage.denominator = DenominatorBasis::Enumerated { count: 0 };
2645 let roll = r.rollup();
2646 assert_eq!(
2647 roll.verdict,
2648 RollupVerdict::Inconclusive,
2649 "0/0 is not a clean bill of health"
2650 );
2651 assert!(
2652 roll.blind_spots.iter().any(|s| s.contains("vacuous")),
2653 "the blindness is named, not implied: {:?}",
2654 roll.blind_spots
2655 );
2656 }
2657
2658 #[test]
2662 fn a_non_enumerable_facet_blocks_green_even_in_a_mixed_binding() {
2663 let mut r = clean_report();
2664 r.capabilities.push(FacetCapability {
2665 facet: "site".to_string(),
2666 medium_type: "web".to_string(),
2667 enumerable: false,
2668 change_signal: true,
2672 base_version_retrievable: false,
2673 anchor_namespace: "url".to_string(),
2674 signal: "none".to_string(),
2675 });
2676 assert!(matches!(
2678 r.coverage.denominator,
2679 DenominatorBasis::Enumerated { count } if count > 0
2680 ));
2681 let roll = r.rollup();
2682 assert_eq!(
2683 roll.verdict,
2684 RollupVerdict::Inconclusive,
2685 "one enumerable facet must not launder a non-enumerable one: {roll:?}"
2686 );
2687 assert!(
2688 roll.blind_spots
2689 .iter()
2690 .any(|s| s.contains("not enumerable")),
2691 "{:?}",
2692 roll.blind_spots
2693 );
2694 }
2695
2696 #[test]
2702 fn a_resolved_signal_of_none_blocks_green_even_when_the_medium_could_signal() {
2703 let mut r = clean_report();
2704 r.capabilities[0].change_signal = true;
2707 r.capabilities[0].signal = "none".to_string();
2708 r.freshness[0].change_detectable = false;
2709 r.freshness[0].signal = "none".to_string();
2710 let roll = r.rollup();
2711 assert_eq!(
2712 roll.verdict,
2713 RollupVerdict::Inconclusive,
2714 "a change-blind binding is not a clean bill of health: {roll:?}"
2715 );
2716 assert!(
2717 roll.blind_spots
2718 .iter()
2719 .any(|s| s.contains("could not read that signal")),
2720 "the blind spot names the unreadable signal: {:?}",
2721 roll.blind_spots
2722 );
2723 }
2724
2725 #[test]
2729 fn a_facet_without_a_change_signal_blocks_green() {
2730 let mut r = clean_report();
2731 r.capabilities[0].change_signal = false;
2732 let roll = r.rollup();
2733 assert_eq!(roll.verdict, RollupVerdict::Inconclusive);
2734 assert!(
2735 roll.blind_spots
2736 .iter()
2737 .any(|s| s.contains("no change signal")),
2738 "{:?}",
2739 roll.blind_spots
2740 );
2741 }
2742
2743 #[test]
2746 fn a_non_enumerable_scope_blocks_green() {
2747 let mut r = clean_report();
2748 r.coverage.denominator = DenominatorBasis::NonEnumerable {
2749 reason: "web medium".to_string(),
2750 };
2751 assert_eq!(r.rollup().verdict, RollupVerdict::Inconclusive);
2752 }
2753
2754 #[test]
2756 fn zero_observed_anchors_blocks_green() {
2757 let mut r = clean_report();
2758 r.anchors.observed = 0;
2759 r.anchors.resolves = 0;
2760 assert_eq!(r.rollup().verdict, RollupVerdict::Inconclusive);
2761 }
2762
2763 #[test]
2767 fn adopt_with_only_uncovered_is_never_red() {
2768 let mut r = clean_report();
2769 r.adopt = true;
2770 r.findings_by_class.insert("uncovered".to_string(), 12);
2771 let roll = r.rollup();
2772 assert_eq!(
2773 roll.verdict,
2774 RollupVerdict::Inconclusive,
2775 "onboarding is neither drift nor a clean bill: {roll:?}"
2776 );
2777 assert!(
2778 roll.because.contains("backfill worklist"),
2779 "the reason states the onboarding framing: {}",
2780 roll.because
2781 );
2782
2783 r.findings_by_class.insert("drifted".to_string(), 1);
2786 assert_eq!(r.rollup().verdict, RollupVerdict::Drifted);
2787 }
2788
2789 #[test]
2792 fn findings_outrank_blind_spots() {
2793 let mut r = clean_report();
2794 r.capabilities[0].change_signal = false;
2795 r.findings_by_class.insert("wrong".to_string(), 1);
2796 let roll = r.rollup();
2797 assert_eq!(roll.verdict, RollupVerdict::Drifted);
2798 assert!(
2799 !roll.blind_spots.is_empty(),
2800 "the blindness is still reported alongside the verdict"
2801 );
2802 }
2803
2804 #[test]
2807 fn actions_are_severity_ordered_and_never_drop_a_class() {
2808 let mut r = clean_report();
2809 r.findings_by_class.insert("uncovered".to_string(), 3);
2810 r.findings_by_class.insert("wrong".to_string(), 1);
2811 r.findings_by_class
2812 .insert("some-future-class".to_string(), 2);
2813 let roll = r.rollup();
2814 assert!(
2815 roll.actions[0].contains("contradict their source"),
2816 "{roll:?}"
2817 );
2818 assert_eq!(roll.actions.len(), 3, "{roll:?}");
2819 assert!(
2820 roll.actions.iter().any(|a| a.contains("some-future-class")),
2821 "an unranked class still surfaces: {roll:?}"
2822 );
2823 }
2824
2825 #[test]
2827 fn verdict_wire_strings_are_stable() {
2828 assert_eq!(RollupVerdict::Clean.wire(), "clean");
2829 assert_eq!(RollupVerdict::Drifted.wire(), "drifted");
2830 assert_eq!(RollupVerdict::Inconclusive.wire(), "inconclusive");
2831 let json = serde_json::to_string(&RollupVerdict::Inconclusive).unwrap();
2832 assert_eq!(json, "\"inconclusive\"");
2833 }
2834}