1use std::collections::BTreeMap;
35use std::path::Path;
36
37use serde::Serialize;
38
39use crate::Engine;
40use crate::anchor::{AnchorGrain, AnchorProvenanceClass, AnchorState};
41use crate::binding::{Binding, CoverageSemantics, MediumCapabilities, medium_capabilities};
42use crate::chunking::estimate_tokens;
43
44use super::advance::read_advance_store;
45use super::cursor::{enumerate_facet_files, source_moved};
46use super::findings::{FindingClass, FindingKey, read_findings_store};
47use super::resolve::{ChangeStrategy, ResolvedIngest, ResolvedSource, resolve_change_strategy};
48
49pub const DEFAULT_REPORT_BUDGET: usize = 8_000;
53
54pub const ALLOWED_REPORT_INCLUDE_KEYS: &[&str] =
60 &["uncovered_artifacts", "tree_fanout", "superseded_findings"];
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
73#[serde(tag = "kind", rename_all = "kebab-case")]
74pub enum DenominatorBasis {
75 Enumerated {
78 count: usize,
80 },
81 NonEnumerable {
85 reason: String,
87 },
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
94pub struct TreeFanout {
95 pub entity: String,
97 pub artifact: String,
99 pub fanout: usize,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
107pub struct GrainCoverage {
108 pub denominator: DenominatorBasis,
110 pub direct_covered: usize,
112 pub tree_only_covered: usize,
115 pub uncovered: Vec<String>,
117 pub tree_anchors: Vec<TreeFanout>,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
125pub struct AnchorComposition {
126 pub by_class: BTreeMap<String, usize>,
129 pub by_grain: BTreeMap<String, usize>,
131 pub authored: usize,
134 pub observed: usize,
136 pub resolves: usize,
138 pub drifted: usize,
140 pub recheck: usize,
142 pub orphaned: usize,
144 pub unobserved: usize,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
152pub struct FacetCapability {
153 pub facet: String,
155 pub medium_type: String,
157 pub enumerable: bool,
159 pub change_signal: bool,
161 pub base_version_retrievable: bool,
163 pub anchor_namespace: String,
165 pub signal: String,
168}
169
170impl FacetCapability {
171 fn from_caps(
172 facet: String,
173 medium_type: String,
174 caps: MediumCapabilities,
175 strategy: ChangeStrategy,
176 ) -> Self {
177 FacetCapability {
178 facet,
179 medium_type,
180 enumerable: caps.enumerable,
181 change_signal: caps.change_signal,
182 base_version_retrievable: caps.base_version_retrievable
189 && strategy_retrieves_base(strategy),
190 anchor_namespace: caps.anchor_namespace.to_string(),
191 signal: signal_wire(strategy).to_string(),
192 }
193 }
194}
195
196#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
198pub struct FacetFreshness {
199 pub facet: String,
201 pub signal: String,
203 pub synced: Option<String>,
205 pub verified: Option<String>,
207 pub change_detectable: bool,
212}
213
214#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
216pub struct FidelityReport {
217 pub binding: String,
219 pub destination_mem: String,
221 pub adopt: bool,
228 pub coverage_semantics: CoverageSemantics,
232 pub coverage_semantics_declared: bool,
237 pub capabilities: Vec<FacetCapability>,
239 pub freshness: Vec<FacetFreshness>,
241 pub source_moved_past_synced: Option<bool>,
245 pub coverage: GrainCoverage,
247 pub anchors: AnchorComposition,
249 pub findings_by_class: BTreeMap<String, usize>,
251 pub backlog: usize,
253 pub superseded: Vec<String>,
256 pub disposed_excluded: usize,
259 pub disposed_excluded_rationales: Vec<(String, String)>,
265 pub degradations: Vec<String>,
267}
268
269#[derive(Debug, Clone, PartialEq, Eq)]
276pub struct RenderedFidelityReport {
277 pub markdown: String,
279 pub mode: String,
282 pub hints: Vec<(String, usize)>,
285 pub budget_used: usize,
287}
288
289fn ratio(num: usize, den: usize) -> String {
295 if den == 0 {
296 format!("{num}/{den} (n/a)")
297 } else {
298 let pct = (num as f64) * 100.0 / (den as f64);
299 format!("{num}/{den} ({pct:.1}%)")
300 }
301}
302
303fn render_hard_required(report: &FidelityReport) -> String {
307 let mut md = String::new();
308 md.push_str(&format!("# Fidelity report — `{}`\n\n", report.binding));
309 md.push_str(&format!(
310 "- **Destination mem:** `{}`\n- **Coverage semantics:** {}{}\n\n",
311 report.destination_mem,
312 match report.coverage_semantics {
313 CoverageSemantics::Exhaustive => "exhaustive",
314 CoverageSemantics::Curated => "curated",
315 },
316 if report.coverage_semantics_declared {
317 ""
318 } else {
319 " (resolved from the sources' media — not declared)"
320 }
321 ));
322
323 if report.adopt {
330 md.push_str("## Adopting — first verify\n\n");
331 md.push_str(
332 "This mem predates its binding: it carries no anchors and has no prior sync \
333 baseline, so **0% anchored is expected — this is onboarding, not a failure.** \
334 Do not read the coverage numbers below as drift or a red verdict; the uncovered \
335 artifacts are the backfill worklist, not defects.\n\n",
336 );
337 md.push_str(&format!(
338 "**Backfill path:** run `memstead projection sync {}` to work through the in-scope \
339 source artifacts that carry no entity yet, covering the clearly-new concepts among \
340 them through the normal mutation surface. Backfilling is incremental — a partial \
341 pass is fine, and the next sync continues where you left off.\n\n",
342 report.binding
343 ));
344 }
345
346 md.push_str("## Denominator provenance\n\n");
348 match &report.coverage.denominator {
349 DenominatorBasis::Enumerated { count } => md.push_str(&format!(
350 "Coverage is reported relative to the per-medium enumeration `S(D)` = **{count}** \
351 source artifact(s) in scope (after `deny_paths`).\n\n"
352 )),
353 DenominatorBasis::NonEnumerable { reason } => md.push_str(&format!(
354 "No `S(D)` denominator: {reason}. Coverage is reported over anchors only; the \
355 per-medium enumeration is unavailable.\n\n"
356 )),
357 }
358
359 md.push_str("## Capability matrix\n\n");
361 if report.capabilities.is_empty() {
362 md.push_str("_(no primary sources resolved)_\n\n");
363 } else {
364 for c in &report.capabilities {
365 md.push_str(&format!("### `{}` ({})\n\n", c.facet, c.medium_type));
366 md.push_str(&format!(
367 "- enumerable: {} | change_signal: {} | base_version_retrievable: {}\n",
368 c.enumerable, c.change_signal, c.base_version_retrievable
369 ));
370 md.push_str(&format!(
371 "- anchor_namespace: `{}` | resolved signal: `{}`\n\n",
372 c.anchor_namespace, c.signal
373 ));
374 }
375 }
376
377 md.push_str("## Freshness\n\n");
379 if report.freshness.is_empty() {
380 md.push_str("_(no source facets)_\n\n");
381 } else {
382 for f in &report.freshness {
383 md.push_str(&format!("### `{}`\n\n", f.facet));
384 md.push_str(&format!("- signal: `{}`\n", f.signal));
385 if !f.change_detectable {
386 md.push_str(
390 "- **freshness unknowable** — this medium is not change-detectable \
391 (no change signal); `#synced` / `#verified` cannot be adjudicated as fresh\n",
392 );
393 } else {
394 match &f.synced {
395 Some(t) => md.push_str(&format!("- `#synced`: `{t}`\n")),
396 None => md.push_str("- `#synced`: never synced\n"),
397 }
398 match &f.verified {
399 Some(t) => md.push_str(&format!("- `#verified`: `{t}`\n")),
400 None => md.push_str("- `#verified`: never verified\n"),
401 }
402 }
403 md.push('\n');
404 }
405 match report.source_moved_past_synced {
407 Some(true) => md.push_str(
408 "**Source moved past its `#synced` baseline** — the graph is stale for the \
409 moved facet(s); a sync pass is due.\n\n",
410 ),
411 Some(false) => {
412 md.push_str("Every change-detectable source is at its `#synced` baseline.\n\n")
413 }
414 None => {}
415 }
416 }
417
418 md.push_str("## Coverage (grain-classed)\n\n");
420 let den = match &report.coverage.denominator {
421 DenominatorBasis::Enumerated { count } => *count,
422 DenominatorBasis::NonEnumerable { .. } => 0,
423 };
424 md.push_str(&format!(
425 "- direct-covered (file / span anchors): {}\n",
426 ratio(report.coverage.direct_covered, den)
427 ));
428 let tree_files: usize = report.coverage.tree_anchors.iter().map(|t| t.fanout).sum();
431 md.push_str(&format!(
432 "- tree-anchor fan-out (separate axis): {} tree anchor(s) fanning out over {} file(s); \
433 {} file(s) covered ONLY via a tree anchor\n",
434 report.coverage.tree_anchors.len(),
435 tree_files,
436 report.coverage.tree_only_covered
437 ));
438 md.push_str(&format!(
439 "- uncovered (no anchor): {}\n\n",
440 report.coverage.uncovered.len()
441 ));
442
443 match report.coverage_semantics {
448 CoverageSemantics::Exhaustive if report.adopt => {
449 let backlog = report
450 .coverage
451 .uncovered
452 .len()
453 .saturating_sub(report.disposed_excluded);
454 md.push_str(&format!(
455 "**Exhaustive coverage (onboarding):** {backlog} in-scope artifact(s) carry no \
456 entity yet ({} disposed excluded) — the expected first-sync backfill worklist \
457 for a mem that predates its binding, not defects.\n\n",
458 report.disposed_excluded
459 ));
460 }
461 CoverageSemantics::Exhaustive => {
462 let findings = report
463 .coverage
464 .uncovered
465 .len()
466 .saturating_sub(report.disposed_excluded);
467 md.push_str(&format!(
468 "**Exhaustive coverage:** {findings} unaccounted artifact(s) — not anchored, not \
469 declared-excluded, no persisted disposition ({} disposed excluded) — are \
470 **findings**.\n\n",
471 report.disposed_excluded
472 ));
473 }
474 CoverageSemantics::Curated => {
475 md.push_str(&format!(
476 "**Curated coverage:** {} unaccounted artifact(s) are **information**, not \
477 defects — a curated binding covers a deliberate slice.\n\n",
478 report.coverage.uncovered.len()
479 ));
480 }
481 }
482
483 if !report.disposed_excluded_rationales.is_empty() {
487 md.push_str("**Excluded on purpose (persisted dispositions):**\n");
488 for (artifact, rationale) in &report.disposed_excluded_rationales {
489 if rationale.is_empty() {
490 md.push_str(&format!("- `{artifact}`\n"));
491 } else {
492 md.push_str(&format!("- `{artifact}` — {rationale}\n"));
493 }
494 }
495 md.push('\n');
496 }
497
498 md.push_str("## Anchors\n\n");
500 md.push_str(&format!(
501 "- by class: {}\n",
502 render_counts(&report.anchors.by_class)
503 ));
504 md.push_str(&format!(
505 "- by grain: {}\n",
506 render_counts(&report.anchors.by_grain)
507 ));
508 md.push_str(&format!(
509 "- `authored` bucket (excluded from coverage/accuracy denominators): {}\n",
510 report.anchors.authored
511 ));
512 md.push_str(&format!(
513 "- resolution (non-`authored`, observed): resolves {}, drifted {}, recheck {}, orphaned {}\n",
514 report.anchors.resolves,
515 report.anchors.drifted,
516 report.anchors.recheck,
517 report.anchors.orphaned
518 ));
519 md.push_str(&format!(
520 "- **anchor-resolution %:** {}\n",
521 ratio(report.anchors.resolves, report.anchors.observed)
522 ));
523 md.push_str(&format!(
524 "- unobserved this pass (state unavailable, never scored as resolved): {}\n\n",
525 report.anchors.unobserved
526 ));
527
528 md.push_str("## Findings\n\n");
530 md.push_str(&format!(
531 "- by class: {}\n",
532 render_counts(&report.findings_by_class)
533 ));
534 md.push_str(&format!(
535 "- **tier-3 adjudication backlog:** {}\n",
536 report.backlog
537 ));
538 md.push_str(&format!(
539 "- superseded (prior `(hash(D), source_head)` key, segregated): {}\n\n",
540 report.superseded.len()
541 ));
542
543 md.push_str("## Degradations\n\n");
545 if report.degradations.is_empty() {
546 md.push_str("_(none)_\n\n");
547 } else {
548 for d in &report.degradations {
549 md.push_str(&format!("- {d}\n"));
550 }
551 md.push('\n');
552 }
553
554 md
555}
556
557fn render_counts(counts: &BTreeMap<String, usize>) -> String {
559 if counts.is_empty() {
560 return "(none)".to_string();
561 }
562 counts
563 .iter()
564 .map(|(k, v)| format!("{k}={v}"))
565 .collect::<Vec<_>>()
566 .join(", ")
567}
568
569fn heavy_sections(report: &FidelityReport) -> Vec<(&'static str, String)> {
573 let mut out: Vec<(&'static str, String)> = Vec::new();
574
575 let mut s = String::new();
577 if !report.coverage.uncovered.is_empty() {
578 s.push_str("## Uncovered artifacts\n\n");
579 for a in &report.coverage.uncovered {
580 s.push_str(&format!("- `{a}`\n"));
581 }
582 s.push('\n');
583 }
584 out.push(("uncovered_artifacts", s));
585
586 let mut s = String::new();
588 if !report.coverage.tree_anchors.is_empty() {
589 s.push_str("## Tree-anchor fan-out (detail)\n\n");
590 for t in &report.coverage.tree_anchors {
591 s.push_str(&format!(
592 "- `{}` → `{}` fans out over {} file(s)\n",
593 t.entity, t.artifact, t.fanout
594 ));
595 }
596 s.push('\n');
597 }
598 out.push(("tree_fanout", s));
599
600 let mut s = String::new();
602 if !report.superseded.is_empty() {
603 s.push_str("## Superseded findings (detail)\n\n");
604 for f in &report.superseded {
605 s.push_str(&format!("- {f}\n"));
606 }
607 s.push('\n');
608 }
609 out.push(("superseded_findings", s));
610
611 out
612}
613
614pub fn render_fidelity_report(
625 report: &FidelityReport,
626 budget: usize,
627 include: &[String],
628) -> RenderedFidelityReport {
629 let hard = render_hard_required(report);
630 let hard_cost = estimate_tokens(&hard);
631 let overbudget = hard_cost > budget;
632
633 let include_set: std::collections::BTreeSet<&str> = include
634 .iter()
635 .map(String::as_str)
636 .filter(|k| ALLOWED_REPORT_INCLUDE_KEYS.contains(k))
637 .collect();
638 let unknown_includes: Vec<&String> = include
639 .iter()
640 .filter(|k| !ALLOWED_REPORT_INCLUDE_KEYS.contains(&k.as_str()))
641 .collect();
642
643 let sections = heavy_sections(report);
644 let mut emitted: Vec<String> = Vec::new();
645 let mut hints: Vec<(String, usize)> = Vec::new();
646 let mut used = hard_cost;
647 let mut remaining = budget.saturating_sub(hard_cost);
648
649 for (key, section_md) in §ions {
650 if section_md.is_empty() {
651 continue; }
653 let cost = estimate_tokens(section_md);
654 let forced = include_set.contains(key);
655 if forced {
656 emitted.push(section_md.clone());
657 used += cost;
658 remaining = remaining.saturating_sub(cost);
659 } else if !overbudget && remaining >= cost {
660 emitted.push(section_md.clone());
661 used += cost;
662 remaining -= cost;
663 } else {
664 hints.push(((*key).to_string(), cost));
665 }
666 }
667
668 let mode = if overbudget {
669 "overbudget"
670 } else if hints.is_empty() {
671 "complete"
672 } else {
673 "reduced"
674 };
675
676 let mut md = String::new();
677 md.push_str("---\n");
678 md.push_str(&format!("_report_mode: {mode}\n"));
679 md.push_str(&format!("_budget_requested: {budget}\n"));
680 md.push_str(&format!("_budget_used: {used}\n"));
681 md.push_str("---\n\n");
682 md.push_str(&hard);
683 for section in &emitted {
684 md.push_str(section);
685 }
686
687 if !hints.is_empty() {
688 md.push_str("## Hints\n\n");
689 md.push_str(
690 "_(heavy sections omitted under the token budget — re-query with the key)_\n\n",
691 );
692 for (key, tokens) in &hints {
693 md.push_str(&format!("- `{key}` — estimated_tokens: {tokens}\n"));
694 }
695 md.push('\n');
696 }
697
698 if !unknown_includes.is_empty() {
699 md.push_str("## Warnings\n\n");
700 for k in &unknown_includes {
701 md.push_str(&format!(
702 "- unknown include key `{k}` — allowed: {}\n",
703 ALLOWED_REPORT_INCLUDE_KEYS.join(", ")
704 ));
705 }
706 md.push('\n');
707 }
708
709 RenderedFidelityReport {
710 markdown: md,
711 mode: mode.to_string(),
712 hints,
713 budget_used: used,
714 }
715}
716
717pub fn compute_fidelity_report(
732 engine: &Engine,
733 workspace_root: &Path,
734 binding: &Binding,
735 resolved: &ResolvedIngest,
736 key: &FindingKey,
737) -> FidelityReport {
738 let binding_id = resolved.name.clone();
739 let dest = resolved.destination_mem.clone();
740
741 let sync_state = engine
743 .mem_config_for(&dest)
744 .map(|c| c.sync_state.clone())
745 .unwrap_or_default();
746 let mut capabilities: Vec<FacetCapability> = Vec::new();
747 let mut freshness: Vec<FacetFreshness> = Vec::new();
748 let mut any_change_detectable = false;
749 for source in &resolved.sources {
750 let ResolvedSource::Primary(p) = source else {
751 continue;
752 };
753 let caps = medium_capabilities(p.medium_type);
754 let medium_type = serde_json::to_value(p.medium_type)
755 .ok()
756 .and_then(|v| v.as_str().map(str::to_string))
757 .unwrap_or_default();
758 let strategy = resolve_change_strategy(p, workspace_root);
759 let signal = signal_wire(strategy).to_string();
760 let change_detectable = caps.change_signal && strategy != ChangeStrategy::None;
761 any_change_detectable |= change_detectable;
762
763 capabilities.push(FacetCapability::from_caps(
764 p.name.clone(),
765 medium_type,
766 caps,
767 strategy,
768 ));
769
770 let synced = sync_state
771 .get(&format!("{binding_id}/{}#synced", p.name))
772 .cloned();
773 let verified = sync_state
774 .get(&format!("{binding_id}/{}#verified", p.name))
775 .cloned();
776 freshness.push(FacetFreshness {
777 facet: p.name.clone(),
778 signal,
779 synced,
780 verified,
781 change_detectable,
782 });
783 }
784
785 let source_moved_past_synced = if any_change_detectable {
786 Some(source_moved(engine, resolved, workspace_root))
787 } else {
788 None
789 };
790
791 let mut s_d: Vec<String> = Vec::new();
793 let mut enumerable_facets = 0usize;
794 for source in &resolved.sources {
795 if let ResolvedSource::Primary(p) = source {
796 let caps = medium_capabilities(p.medium_type);
797 if caps.enumerable {
798 enumerable_facets += 1;
799 }
800 s_d.extend(enumerate_facet_files(
801 p,
802 &resolved.deny_paths,
803 workspace_root,
804 ));
805 }
806 }
807 s_d.sort();
808 s_d.dedup();
809
810 let denominator = if !s_d.is_empty() {
811 DenominatorBasis::Enumerated { count: s_d.len() }
812 } else if enumerable_facets == 0 {
813 DenominatorBasis::NonEnumerable {
814 reason: "the medium type(s) are not enumerable this cycle".to_string(),
815 }
816 } else {
817 DenominatorBasis::NonEnumerable {
820 reason: "no source artifacts enumerated in scope".to_string(),
821 }
822 };
823
824 let mut direct_covered = 0usize;
825 let mut tree_only_covered = 0usize;
826 let mut uncovered: Vec<String> = Vec::new();
827 let mut tree_fanout: BTreeMap<(String, String), usize> = BTreeMap::new();
828 for file in &s_d {
829 let refs = engine.anchors_referencing_artifact(file);
830 let mine: Vec<&(crate::EntityId, crate::anchor::Anchor)> = refs
831 .iter()
832 .filter(|(eid, _)| eid.mem() == dest.as_str())
833 .collect();
834 if mine.is_empty() {
835 uncovered.push(file.clone());
836 continue;
837 }
838 let has_non_tree = mine.iter().any(|(_, a)| a.grain != AnchorGrain::Tree);
839 if has_non_tree {
840 direct_covered += 1;
841 } else {
842 tree_only_covered += 1;
843 }
844 for (eid, a) in &mine {
846 if a.grain == AnchorGrain::Tree {
847 *tree_fanout
848 .entry((eid.as_ref().to_string(), a.artifact.clone()))
849 .or_insert(0) += 1;
850 }
851 }
852 }
853 let tree_anchors: Vec<TreeFanout> = tree_fanout
854 .into_iter()
855 .map(|((entity, artifact), fanout)| TreeFanout {
856 entity,
857 artifact,
858 fanout,
859 })
860 .collect();
861
862 let coverage = GrainCoverage {
863 denominator,
864 direct_covered,
865 tree_only_covered,
866 uncovered: uncovered.clone(),
867 tree_anchors,
868 };
869
870 let mut anchors = AnchorComposition::default();
872 for (_eid, resolved_anchor) in engine.mem_anchors_resolved(&dest) {
873 let a = &resolved_anchor.anchor;
874 *anchors
875 .by_class
876 .entry(a.class.as_wire().to_string())
877 .or_insert(0) += 1;
878 *anchors
879 .by_grain
880 .entry(a.grain.as_wire().to_string())
881 .or_insert(0) += 1;
882 if a.class == AnchorProvenanceClass::Authored {
883 anchors.authored += 1;
884 continue; }
886 match resolved_anchor.state {
887 Some(AnchorState::Resolves) => {
888 anchors.resolves += 1;
889 anchors.observed += 1;
890 }
891 Some(AnchorState::Drifted) => {
892 anchors.drifted += 1;
893 anchors.observed += 1;
894 }
895 Some(AnchorState::Recheck) => {
896 anchors.recheck += 1;
897 anchors.observed += 1;
898 }
899 Some(AnchorState::Orphaned) => {
900 anchors.orphaned += 1;
901 anchors.observed += 1;
902 }
903 None => anchors.unobserved += 1,
904 }
905 }
906
907 let mut findings_by_class: BTreeMap<String, usize> = BTreeMap::new();
909 let mut backlog = 0usize;
910 let mut superseded: Vec<String> = Vec::new();
911 if let Some((mem, name)) = binding_id.split_once('/')
912 && let Ok(Some(store)) = read_findings_store(workspace_root, mem, name)
913 {
914 for f in store.current(key) {
915 *findings_by_class
916 .entry(f.class.as_wire().to_string())
917 .or_insert(0) += 1;
918 if f.class == FindingClass::QueuedForAdjudication {
919 backlog += 1;
920 }
921 }
922 for f in store.superseded(key) {
923 superseded.push(format!(
924 "[{}] {} ({})",
925 f.class.as_wire(),
926 finding_target_label(&f.target),
927 f.facet
928 ));
929 }
930 }
931
932 let mut disposed_excluded_rationales: Vec<(String, String)> = Vec::new();
938 if let Some((mem, name)) = binding_id.split_once('/')
939 && let Ok(Some(state)) = read_advance_store(workspace_root, mem, name)
940 {
941 let uncovered_set: std::collections::BTreeSet<&str> =
942 uncovered.iter().map(String::as_str).collect();
943 for (artifact, rationale) in &state.exclusions {
944 if uncovered_set.contains(artifact.as_str()) {
945 disposed_excluded_rationales.push((artifact.clone(), rationale.clone()));
946 }
947 }
948 }
949 let disposed_excluded = disposed_excluded_rationales.len();
950
951 let mut degradations: Vec<String> = Vec::new();
953 for c in &capabilities {
954 if !c.change_signal || c.signal == "none" {
955 degradations.push(format!(
956 "change-signal-none:`{}` — freshness is unknowable for this facet",
957 c.facet
958 ));
959 }
960 if !c.enumerable {
961 degradations.push(format!(
962 "enumeration-unavailable:`{}` — `S(D)` coverage denominator not computable",
963 c.facet
964 ));
965 }
966 if !c.base_version_retrievable {
967 degradations.push(format!(
968 "base-version-unretrievable:`{}` — prune degrades to conflict-flagging",
969 c.facet
970 ));
971 }
972 }
973 if anchors.recheck > 0 {
974 degradations.push(format!(
975 "hash-adjudication-deferred — {} anchor(s) recheck (unstable medium / hash \
976 unavailable), not asserted drift",
977 anchors.recheck
978 ));
979 }
980 if anchors.unobserved > 0 {
981 degradations.push(format!(
982 "anchors-unobserved — {} anchor(s) could not be observed this pass",
983 anchors.unobserved
984 ));
985 }
986
987 let adopt = super::render::mem_predates_binding(engine, resolved);
991 let effective_coverage = crate::binding::effective_coverage_semantics(binding);
992
993 FidelityReport {
994 binding: binding_id,
995 destination_mem: dest,
996 adopt,
997 coverage_semantics: effective_coverage.value,
998 coverage_semantics_declared: effective_coverage.declared,
999 capabilities,
1000 freshness,
1001 source_moved_past_synced,
1002 coverage,
1003 anchors,
1004 findings_by_class,
1005 backlog,
1006 superseded,
1007 disposed_excluded,
1008 disposed_excluded_rationales,
1009 degradations,
1010 }
1011}
1012
1013fn strategy_retrieves_base(strategy: ChangeStrategy) -> bool {
1022 matches!(strategy, ChangeStrategy::Git | ChangeStrategy::Graph)
1023}
1024
1025fn signal_wire(strategy: ChangeStrategy) -> &'static str {
1028 match strategy {
1029 ChangeStrategy::None => "none",
1030 ChangeStrategy::Git => "git",
1031 ChangeStrategy::Mtime => "mtime",
1032 ChangeStrategy::Graph => "graph",
1033 }
1034}
1035
1036fn finding_target_label(target: &super::findings::FindingTarget) -> String {
1038 match target {
1039 super::findings::FindingTarget::Anchor { entity, artifact } => {
1040 format!("{entity} → {artifact}")
1041 }
1042 super::findings::FindingTarget::Artifact { artifact } => artifact.clone(),
1043 }
1044}
1045
1046#[cfg(test)]
1047mod tests {
1048 use super::*;
1049
1050 fn base_report() -> FidelityReport {
1053 FidelityReport {
1054 binding: "engine/graph".to_string(),
1055 destination_mem: "engine".to_string(),
1056 adopt: false,
1057 coverage_semantics: CoverageSemantics::Exhaustive,
1058 coverage_semantics_declared: true,
1059 capabilities: vec![FacetCapability {
1060 facet: "src".to_string(),
1061 medium_type: "codebase".to_string(),
1062 enumerable: true,
1063 change_signal: true,
1064 base_version_retrievable: true,
1065 anchor_namespace: "path".to_string(),
1066 signal: "git".to_string(),
1067 }],
1068 freshness: vec![FacetFreshness {
1069 facet: "src".to_string(),
1070 signal: "git".to_string(),
1071 synced: Some("deadbeef".to_string()),
1072 verified: None,
1073 change_detectable: true,
1074 }],
1075 source_moved_past_synced: Some(false),
1076 coverage: GrainCoverage {
1077 denominator: DenominatorBasis::Enumerated { count: 10 },
1078 direct_covered: 6,
1079 tree_only_covered: 3,
1080 uncovered: vec!["src/a.rs".to_string()],
1081 tree_anchors: vec![TreeFanout {
1082 entity: "engine--big".to_string(),
1083 artifact: "src/".to_string(),
1084 fanout: 3,
1085 }],
1086 },
1087 anchors: AnchorComposition {
1088 by_class: BTreeMap::from([
1089 ("anchored".to_string(), 5),
1090 ("authored".to_string(), 2),
1091 ]),
1092 by_grain: BTreeMap::from([("file".to_string(), 4), ("tree".to_string(), 1)]),
1093 authored: 2,
1094 observed: 5,
1095 resolves: 4,
1096 drifted: 0,
1097 recheck: 1,
1098 orphaned: 0,
1099 unobserved: 0,
1100 },
1101 findings_by_class: BTreeMap::from([
1102 ("uncovered".to_string(), 1),
1103 ("queued-for-adjudication".to_string(), 1),
1104 ]),
1105 backlog: 1,
1106 superseded: Vec::new(),
1107 disposed_excluded: 0,
1108 disposed_excluded_rationales: Vec::new(),
1109 degradations: vec!["hash-adjudication-deferred — 1 anchor(s) recheck".to_string()],
1110 }
1111 }
1112
1113 #[test]
1118 fn b1_renders_all_elements_deterministically() {
1119 let r = base_report();
1120 let a = render_fidelity_report(&r, 8_000, &[]);
1121 let b = render_fidelity_report(&r, 8_000, &[]);
1122 assert_eq!(a.markdown, b.markdown, "deterministic — identical bytes");
1123
1124 let md = &a.markdown;
1125 assert!(md.contains("direct-covered (file / span anchors): 6/10"));
1127 assert!(md.contains(
1128 "tree-anchor fan-out (separate axis): 1 tree anchor(s) fanning out over 3 file(s)"
1129 ));
1130 assert!(
1132 !md.contains("9/10"),
1133 "tree fan-out must not blend into direct coverage"
1134 );
1135 assert!(md.contains("anchor-resolution %:** 4/5"));
1137 assert!(md.contains("`authored` bucket (excluded from coverage/accuracy denominators): 2"));
1139 assert!(md.contains("tier-3 adjudication backlog:** 1"));
1141 assert!(md.contains("## Capability matrix"));
1143 assert!(md.contains("## Degradations"));
1144 assert!(md.contains("hash-adjudication-deferred"));
1145 assert!(md.contains("per-medium enumeration `S(D)` = **10**"));
1147 }
1148
1149 #[test]
1152 fn b2_detectionless_medium_freshness_unknowable_never_green() {
1153 let mut r = base_report();
1154 r.capabilities = vec![FacetCapability {
1155 facet: "manual".to_string(),
1156 medium_type: "web".to_string(),
1157 enumerable: false,
1158 change_signal: false,
1159 base_version_retrievable: false,
1160 anchor_namespace: "url".to_string(),
1161 signal: "none".to_string(),
1162 }];
1163 r.freshness = vec![FacetFreshness {
1164 facet: "manual".to_string(),
1165 signal: "none".to_string(),
1166 synced: Some("should-never-render-green".to_string()),
1169 verified: Some("nor-this".to_string()),
1170 change_detectable: false,
1171 }];
1172 r.source_moved_past_synced = None;
1173 let out = render_fidelity_report(&r, 8_000, &[]);
1174 let md = &out.markdown;
1175 assert!(md.contains("signal: `none`"));
1176 assert!(md.contains("freshness unknowable"));
1177 assert!(!md.contains("should-never-render-green"));
1180 assert!(
1181 !md.contains("`#synced`: `"),
1182 "no synced token rendered for a non-detectable medium"
1183 );
1184 assert!(
1185 !md.contains("at its `#synced` baseline"),
1186 "no green 'at baseline' verdict"
1187 );
1188 }
1189
1190 #[test]
1198 fn b1_base_retrievability_follows_resolved_strategy_not_medium_ceiling() {
1199 use crate::pipeline::MediumType;
1200
1201 assert!(medium_capabilities(MediumType::Filesystem).base_version_retrievable);
1203
1204 let fs_mtime = FacetCapability::from_caps(
1206 "prose".to_string(),
1207 "filesystem".to_string(),
1208 medium_capabilities(MediumType::Filesystem),
1209 ChangeStrategy::Mtime,
1210 );
1211 assert!(
1212 !fs_mtime.base_version_retrievable,
1213 "filesystem+mtime has no retrievable base leg — degrades to conflict-flag"
1214 );
1215 assert_eq!(fs_mtime.signal, "mtime");
1216
1217 let fs_git = FacetCapability::from_caps(
1218 "prose".to_string(),
1219 "filesystem".to_string(),
1220 medium_capabilities(MediumType::Filesystem),
1221 ChangeStrategy::Git,
1222 );
1223 assert!(
1224 fs_git.base_version_retrievable,
1225 "filesystem backed by git keeps the never-clobber base leg"
1226 );
1227
1228 assert!(!strategy_retrieves_base(ChangeStrategy::None));
1230 assert!(!strategy_retrieves_base(ChangeStrategy::Mtime));
1231 assert!(strategy_retrieves_base(ChangeStrategy::Git));
1232 assert!(strategy_retrieves_base(ChangeStrategy::Graph));
1233
1234 let mut r = base_report();
1238 r.capabilities = vec![fs_mtime.clone()];
1239 r.degradations = if !fs_mtime.base_version_retrievable {
1240 vec![format!(
1241 "base-version-unretrievable:`{}` — prune degrades to conflict-flagging",
1242 fs_mtime.facet
1243 )]
1244 } else {
1245 Vec::new()
1246 };
1247 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
1248 assert!(
1249 md.contains("base-version-unretrievable:`prose` — prune degrades to conflict-flagging"),
1250 "filesystem+mtime surfaces the conflict-flag degradation in the report"
1251 );
1252 }
1253
1254 #[test]
1257 fn b3_aggregates_always_ship_at_zero_budget() {
1258 let r = base_report();
1259 let out = render_fidelity_report(&r, 0, &[]);
1260 assert_eq!(out.mode, "overbudget");
1261 let md = &out.markdown;
1262 assert!(md.contains("direct-covered (file / span anchors): 6/10"));
1264 assert!(md.contains("tier-3 adjudication backlog:** 1"));
1265 assert!(md.contains("## Capability matrix"));
1266 assert!(!md.contains("## Uncovered artifacts"));
1268 assert!(md.contains("## Hints"));
1269 assert!(out.hints.iter().any(|(k, _)| k == "uncovered_artifacts"));
1270 }
1271
1272 #[test]
1276 fn b3_large_facet_list_truncates_then_include_forces() {
1277 let mut r = base_report();
1278 r.coverage.uncovered = (0..500).map(|i| format!("src/file_{i}.rs")).collect();
1280 let hard_cost = estimate_tokens(&render_hard_required(&r));
1282 let out = render_fidelity_report(&r, hard_cost + 5, &[]);
1283 assert_eq!(out.mode, "reduced");
1284 assert!(
1285 !out.markdown.contains("src/file_499.rs"),
1286 "big list not rendered unbounded"
1287 );
1288 assert!(out.markdown.contains("## Hints"));
1289 let (_, est) = out
1290 .hints
1291 .iter()
1292 .find(|(k, _)| k == "uncovered_artifacts")
1293 .expect("uncovered list hinted");
1294 assert!(*est > 5, "the hint carries a real estimated_tokens figure");
1295
1296 let forced =
1298 render_fidelity_report(&r, hard_cost + 5, &["uncovered_artifacts".to_string()]);
1299 assert!(
1300 forced.markdown.contains("src/file_499.rs"),
1301 "include forces the full list"
1302 );
1303 }
1304
1305 #[test]
1308 fn b4_curated_vs_exhaustive_framing() {
1309 let mut exhaustive = base_report();
1310 exhaustive.coverage_semantics = CoverageSemantics::Exhaustive;
1311 let ex_md = render_fidelity_report(&exhaustive, 8_000, &[]).markdown;
1312 assert!(ex_md.contains("Exhaustive coverage:"));
1313 assert!(ex_md.contains("are **findings**"));
1314
1315 let mut curated = base_report();
1316 curated.coverage_semantics = CoverageSemantics::Curated;
1317 let cur_md = render_fidelity_report(&curated, 8_000, &[]).markdown;
1318 assert!(cur_md.contains("Curated coverage:"));
1319 assert!(cur_md.contains("**information**"));
1320 assert!(
1321 !cur_md.contains("are **findings**"),
1322 "curated never frames unaccounted as findings"
1323 );
1324 }
1325
1326 #[test]
1329 fn b4_disposition_excludes_from_exhaustive_findings() {
1330 let mut r = base_report();
1331 r.coverage_semantics = CoverageSemantics::Exhaustive;
1332 r.coverage.uncovered = vec!["src/a.rs".to_string(), "src/b.rs".to_string()];
1333 r.disposed_excluded = 1;
1334 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
1335 assert!(md.contains("1 unaccounted artifact(s)"));
1337 assert!(md.contains("(1 disposed excluded)"));
1338 }
1339
1340 #[test]
1343 fn b4_authored_exclusion_rationale_is_rendered() {
1344 let mut r = base_report();
1345 r.coverage_semantics = CoverageSemantics::Exhaustive;
1346 r.coverage.uncovered = vec!["src/gen.rs".to_string()];
1347 r.disposed_excluded = 1;
1348 r.disposed_excluded_rationales =
1349 vec![("src/gen.rs".to_string(), "generated; no entity".to_string())];
1350 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
1351 assert!(md.contains("Excluded on purpose (persisted dispositions):"));
1352 assert!(md.contains("`src/gen.rs` — generated; no entity"));
1353 }
1354
1355 #[test]
1358 fn b5_denominator_provenance_stated() {
1359 let r = base_report();
1360 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
1361 assert!(md.contains("## Denominator provenance"));
1362 assert!(md.contains("per-medium enumeration `S(D)` = **10**"));
1363
1364 let mut non = base_report();
1365 non.coverage.denominator = DenominatorBasis::NonEnumerable {
1366 reason: "the medium type(s) are not enumerable this cycle".to_string(),
1367 };
1368 let md2 = render_fidelity_report(&non, 8_000, &[]).markdown;
1369 assert!(md2.contains("No `S(D)` denominator"));
1370 assert!(md2.contains("not enumerable this cycle"));
1371 }
1372
1373 #[test]
1379 fn e1_adopt_report_renders_onboarding_no_red_verdict() {
1380 let mut r = base_report();
1381 r.adopt = true;
1382 r.coverage_semantics = CoverageSemantics::Exhaustive;
1383 r.coverage.uncovered = (0..5).map(|i| format!("src/file_{i}.rs")).collect();
1384 let md = render_fidelity_report(&r, 8_000, &[]).markdown;
1385
1386 assert!(md.contains("## Adopting — first verify"));
1388 assert!(md.contains("0% anchored is expected — this is onboarding, not a failure."));
1389 assert!(md.contains("**Backfill path:** run `memstead projection sync engine/graph`"));
1391 assert!(
1394 !md.contains("are **findings**"),
1395 "pre-binding history must not produce a red findings verdict"
1396 );
1397 assert!(md.contains("Exhaustive coverage (onboarding):"));
1398 assert!(md.contains("backfill worklist"));
1399
1400 r.adopt = false;
1402 let md2 = render_fidelity_report(&r, 8_000, &[]).markdown;
1403 assert!(!md2.contains("## Adopting — first verify"));
1404 assert!(md2.contains("are **findings**"));
1405 }
1406
1407 #[test]
1409 fn unknown_include_key_warns() {
1410 let r = base_report();
1411 let out = render_fidelity_report(&r, 8_000, &["bogus".to_string()]);
1412 assert!(out.markdown.contains("unknown include key `bogus`"));
1413 }
1414
1415 use crate::anchor::{Anchor, AnchorHashStability, AnchorProvenanceClass, AnchorSidecar};
1418 use crate::binding::{
1419 BINDING_VERSION, Binding, BuildMode, BuildOperation, DEFAULT_ADJUDICATION_CAP,
1420 DEFAULT_FULL_RESYNC_EVERY, Operations, VerifyOperation,
1421 };
1422 use crate::ingest::findings::verify_binding;
1423 use crate::ingest::resolve::resolve_binding_run;
1424 use crate::pipeline::{IngestTrigger, MediumType, PatternEntry, PatternMode};
1425 use crate::pipeline_store::{load_pipeline_configs, write_binding};
1426 use crate::workspace::{
1427 Mount, MountCapability, MountLifecycle, MountStorage, Workspace, WorkspaceSettings,
1428 };
1429 use crate::workspace_store::WorkspaceStoreAdapter;
1430
1431 #[test]
1438 fn compute_report_end_to_end() {
1439 let tmp = tempfile::tempdir().unwrap();
1440 let root = tmp.path();
1441 let mem_dir = root.join("mem");
1442 std::fs::create_dir_all(mem_dir.join(".memstead")).unwrap();
1443 std::fs::write(
1444 mem_dir.join(".memstead").join("config.json"),
1445 r#"{"format":1,"schema":"default@1.0.0","version":"1.0.0"}"#,
1446 )
1447 .unwrap();
1448
1449 std::fs::create_dir_all(root.join(".memstead")).unwrap();
1450 std::fs::write(
1451 root.join(".memstead").join("workspace.toml"),
1452 "format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
1453 )
1454 .unwrap();
1455 let mount = Mount {
1456 mem: "engine".to_string(),
1457 schema: Some("default@1.0.0".parse().unwrap()),
1458 storage: MountStorage::Folder {
1459 path: mem_dir.clone(),
1460 },
1461 capability: MountCapability::Write,
1462 lifecycle: MountLifecycle::Eager,
1463 cross_linkable: false,
1464 migration_target: None,
1465 };
1466 crate::FileWorkspaceStore::new()
1467 .save_state(
1468 root,
1469 &Workspace {
1470 mounts: vec![mount],
1471 settings: WorkspaceSettings::default(),
1472 },
1473 )
1474 .unwrap();
1475
1476 let out = std::process::Command::new("git")
1477 .args(["init", "-q"])
1478 .current_dir(root)
1479 .output()
1480 .unwrap();
1481 assert!(out.status.success());
1482 std::fs::create_dir_all(root.join("src").join("sub")).unwrap();
1483 std::fs::write(root.join("src").join("present.rs"), "fn a() {}\n").unwrap();
1484 std::fs::write(root.join("src").join("uncovered.rs"), "fn b() {}\n").unwrap();
1485 std::fs::write(root.join("src").join("sub").join("deep.rs"), "fn c() {}\n").unwrap();
1486
1487 let mk = |artifact: &str, grain: AnchorGrain, class: AnchorProvenanceClass| Anchor {
1488 artifact: artifact.to_string(),
1489 grain,
1490 class,
1491 at_version: None,
1492 hash: class.is_hash_bearing().then(|| "recorded".to_string()),
1493 hash_stability: AnchorHashStability::Stable,
1494 derived_from: Vec::new(),
1495 binding: None,
1496 source: None,
1497 };
1498 let mut sidecar = AnchorSidecar::default();
1499 sidecar.set(
1500 "engine--direct",
1501 vec![mk(
1502 "src/present.rs",
1503 AnchorGrain::File,
1504 AnchorProvenanceClass::Anchored,
1505 )],
1506 );
1507 sidecar.set(
1508 "engine--tree",
1509 vec![mk(
1510 "src/sub/",
1511 AnchorGrain::Tree,
1512 AnchorProvenanceClass::Anchored,
1513 )],
1514 );
1515 sidecar.set(
1517 "engine--auth",
1518 vec![mk(
1519 "src/present.rs",
1520 AnchorGrain::File,
1521 AnchorProvenanceClass::Authored,
1522 )],
1523 );
1524 std::fs::write(
1525 mem_dir.join(crate::anchor::ANCHOR_SIDECAR_PATH),
1526 sidecar.to_bytes(),
1527 )
1528 .unwrap();
1529
1530 write_binding(
1531 root,
1532 "engine",
1533 "graph",
1534 &Binding {
1535 version: BINDING_VERSION,
1536 intent: None,
1537 sources: vec![crate::pipeline::Source {
1538 name: "graph".to_string(),
1539 medium_type: MediumType::Codebase,
1540 pointer: String::new(),
1541 change_detection: Some("git".to_string()),
1542 scope: vec![PatternEntry {
1543 path: "src/**/*.rs".to_string(),
1544 mode: PatternMode::Allow,
1545 }],
1546 engagement: None,
1547 preparation: None,
1548 }],
1549 reference_mems: Vec::new(),
1550 destination_mem: "engine".to_string(),
1551 deny_paths: Vec::new(),
1552 coverage_semantics: None,
1553 rules: None,
1554 prune: None,
1555 operations: Operations {
1556 build: Some(BuildOperation {
1557 mode: BuildMode::Discovery,
1558 trigger: IngestTrigger::Loop,
1559 batch_size: 20,
1560 post_actions: None,
1561 }),
1562 sync: None,
1563 verify: Some(VerifyOperation {
1564 trigger: IngestTrigger::Manual,
1565 batch_size: 20,
1566 adjudication_cap: DEFAULT_ADJUDICATION_CAP,
1567 full_resync_every: DEFAULT_FULL_RESYNC_EVERY,
1568 }),
1569 },
1570 },
1571 )
1572 .unwrap();
1573
1574 let engine = Engine::from_workspace_root(root).unwrap();
1575 let configs = load_pipeline_configs(root).unwrap();
1576 let binding = &configs.bindings[0].config;
1577 let resolved = resolve_binding_run("engine/graph", binding).unwrap();
1578
1579 let outcome = verify_binding(&engine, root, binding, &resolved).unwrap();
1581
1582 let report = compute_fidelity_report(&engine, root, binding, &resolved, &outcome.key);
1584
1585 assert_eq!(
1587 report.coverage.denominator,
1588 DenominatorBasis::Enumerated { count: 3 }
1589 );
1590 assert_eq!(report.coverage.direct_covered, 1);
1593 assert_eq!(report.coverage.tree_only_covered, 1);
1594 assert_eq!(
1595 report.coverage.uncovered,
1596 vec!["src/uncovered.rs".to_string()]
1597 );
1598 assert_eq!(report.coverage.tree_anchors.len(), 1);
1600 assert_eq!(report.coverage.tree_anchors[0].fanout, 1);
1601 assert_eq!(report.coverage.tree_anchors[0].artifact, "src/sub/");
1602 assert_eq!(report.anchors.authored, 1);
1604 assert_eq!(report.anchors.by_class.get("authored"), Some(&1));
1605 assert_eq!(report.anchors.observed, 2);
1610 assert_eq!(report.anchors.recheck, 1);
1611 assert_eq!(report.anchors.drifted, 1);
1612 assert_eq!(report.backlog, outcome.backlog);
1614 assert!(
1616 report
1617 .degradations
1618 .iter()
1619 .any(|d| d.contains("hash-adjudication-deferred"))
1620 );
1621 let md = render_fidelity_report(&report, 8_000, &[]).markdown;
1623 assert!(md.contains("per-medium enumeration `S(D)` = **3**"));
1624 assert!(!report.adopt);
1627 assert!(!md.contains("## Adopting — first verify"));
1628 }
1629
1630 #[test]
1635 fn compute_report_adopt_when_mem_predates_binding() {
1636 let tmp = tempfile::tempdir().unwrap();
1637 let root = tmp.path();
1638 let mem_dir = root.join("mem");
1639 std::fs::create_dir_all(mem_dir.join(".memstead")).unwrap();
1640 std::fs::write(
1641 mem_dir.join(".memstead").join("config.json"),
1642 r#"{"format":1,"schema":"default@1.0.0","version":"1.0.0"}"#,
1643 )
1644 .unwrap();
1645 std::fs::create_dir_all(root.join(".memstead")).unwrap();
1646 std::fs::write(
1647 root.join(".memstead").join("workspace.toml"),
1648 "format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
1649 )
1650 .unwrap();
1651 let mount = Mount {
1652 mem: "engine".to_string(),
1653 schema: Some("default@1.0.0".parse().unwrap()),
1654 storage: MountStorage::Folder {
1655 path: mem_dir.clone(),
1656 },
1657 capability: MountCapability::Write,
1658 lifecycle: MountLifecycle::Eager,
1659 cross_linkable: false,
1660 migration_target: None,
1661 };
1662 crate::FileWorkspaceStore::new()
1663 .save_state(
1664 root,
1665 &Workspace {
1666 mounts: vec![mount],
1667 settings: WorkspaceSettings::default(),
1668 },
1669 )
1670 .unwrap();
1671 let out = std::process::Command::new("git")
1672 .args(["init", "-q"])
1673 .current_dir(root)
1674 .output()
1675 .unwrap();
1676 assert!(out.status.success());
1677 std::fs::create_dir_all(root.join("src")).unwrap();
1678 std::fs::write(root.join("src").join("a.rs"), "fn a() {}\n").unwrap();
1680 std::fs::write(root.join("src").join("b.rs"), "fn b() {}\n").unwrap();
1681
1682 write_binding(
1683 root,
1684 "engine",
1685 "graph",
1686 &Binding {
1687 version: BINDING_VERSION,
1688 intent: None,
1689 sources: vec![crate::pipeline::Source {
1690 name: "graph".to_string(),
1691 medium_type: MediumType::Codebase,
1692 pointer: String::new(),
1693 change_detection: Some("git".to_string()),
1694 scope: vec![PatternEntry {
1695 path: "src/**/*.rs".to_string(),
1696 mode: PatternMode::Allow,
1697 }],
1698 engagement: None,
1699 preparation: None,
1700 }],
1701 reference_mems: Vec::new(),
1702 destination_mem: "engine".to_string(),
1703 deny_paths: Vec::new(),
1704 coverage_semantics: None,
1705 rules: None,
1706 prune: None,
1707 operations: Operations {
1708 build: Some(BuildOperation {
1709 mode: BuildMode::Discovery,
1710 trigger: IngestTrigger::Loop,
1711 batch_size: 20,
1712 post_actions: None,
1713 }),
1714 sync: None,
1715 verify: Some(VerifyOperation {
1716 trigger: IngestTrigger::Manual,
1717 batch_size: 20,
1718 adjudication_cap: DEFAULT_ADJUDICATION_CAP,
1719 full_resync_every: DEFAULT_FULL_RESYNC_EVERY,
1720 }),
1721 },
1722 },
1723 )
1724 .unwrap();
1725
1726 let engine = Engine::from_workspace_root(root).unwrap();
1727 let configs = load_pipeline_configs(root).unwrap();
1728 let binding = &configs.bindings[0].config;
1729 let resolved = resolve_binding_run("engine/graph", binding).unwrap();
1730 let outcome = verify_binding(&engine, root, binding, &resolved).unwrap();
1731 let report = compute_fidelity_report(&engine, root, binding, &resolved, &outcome.key);
1732
1733 assert!(
1735 report.adopt,
1736 "a no-anchor, never-synced mem predates its binding"
1737 );
1738 let md = render_fidelity_report(&report, 8_000, &[]).markdown;
1739 assert!(md.contains("## Adopting — first verify"));
1740 assert!(md.contains("0% anchored is expected"));
1741 assert!(!md.contains("are **findings**"));
1743 assert!(md.contains("Exhaustive coverage (onboarding):"));
1744 }
1745
1746 #[test]
1750 fn report_marks_resolved_coverage_semantics() {
1751 let mut resolved = base_report();
1752 resolved.coverage_semantics = CoverageSemantics::Curated;
1753 resolved.coverage_semantics_declared = false;
1754 let md = render_hard_required(&resolved);
1755 assert!(
1756 md.contains("curated (resolved from the sources' media — not declared)"),
1757 "resolved value carries the marker: {md}"
1758 );
1759
1760 let declared = base_report(); let md = render_hard_required(&declared);
1762 assert!(
1763 md.contains("**Coverage semantics:** exhaustive\n"),
1764 "declared value renders bare: {md}"
1765 );
1766 assert!(
1767 !md.contains("(resolved from the sources' media"),
1768 "no resolution marker on a declared value: {md}"
1769 );
1770 }
1771}