krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

7b41195620eadd54d25fa98dcd36735cba906ba3

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-04-22T14:47:24Z

feat(v2.7.0): add provenance, confidence, and reproducibility metadata

* add result provenance across major sections
* introduce confidence levels for ambiguous outputs
* distinguish observed facts from inferred summaries
* expand snapshot metadata for reproducibility
* improve error classification and partial snapshot handling
* include provenance and confidence in export
* add local notes for tracked domains and history
 DomainDig.xcodeproj/project.pbxproj       |  12 +-
 DomainDig/AppVersion.swift                |   7 +
 DomainDig/ContentView.swift               | 226 +++++++++++++++----
 DomainDig/DomainAvailabilityService.swift |   8 +-
 DomainDig/DomainDiffService.swift         |  27 ++-
 DomainDig/DomainViewModel.swift           | 187 ++++++++++++++--
 DomainDig/HistoryView.swift               | 112 +++++++++-
 DomainDig/LookupRuntime.swift             |  12 +-
 DomainDig/Models.swift                    | 138 +++++++++++-
 DomainDig/WatchlistView.swift             |  25 ++-
 DomainInspectionService.swift             | 347 +++++++++++++++++++++++++++---
 DomainReportBuilder.swift                 |  28 +++
 DomainReportExporter.swift                |  98 ++++++++-
 LookupSnapshot.swift                      |  26 ++-
 14 files changed, 1120 insertions(+), 133 deletions(-)

diff --git a/DomainDig.xcodeproj/project.pbxproj b/DomainDig.xcodeproj/project.pbxproj
index 1129505..3e7f5d6 100644
--- a/DomainDig.xcodeproj/project.pbxproj
+++ b/DomainDig.xcodeproj/project.pbxproj
@@ -134,7 +134,7 @@
 		};
 		8BF124FB2F70000100933221 /* DomainDigCLI */ = {
 			isa = PBXNativeTarget;
-			buildConfigurationList = 8BBFF0292F987EF700E8E144 /* Build configuration list */;
+			buildConfigurationList = 8BBFF0292F987EF700E8E144 /* Build configuration list for PBXNativeTarget "DomainDigCLI" */;
 			buildPhases = (
 				8BF124FC2F70000100933221 /* Sources */,
 				8BF124FD2F70000100933221 /* Frameworks */,
@@ -354,7 +354,7 @@
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 19;
+				CURRENT_PROJECT_VERSION = 20;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				ENABLE_PREVIEWS = YES;
 				GENERATE_INFOPLIST_FILE = YES;
@@ -371,7 +371,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 2.6.0;
+				MARKETING_VERSION = 2.7.0;
 				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -390,7 +390,7 @@
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 19;
+				CURRENT_PROJECT_VERSION = 20;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				ENABLE_PREVIEWS = YES;
 				GENERATE_INFOPLIST_FILE = YES;
@@ -407,7 +407,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 2.6.0;
+				MARKETING_VERSION = 2.7.0;
 				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -460,7 +460,7 @@
 			defaultConfigurationIsVisible = 0;
 			defaultConfigurationName = Release;
 		};
-		8BBFF0292F987EF700E8E144 /* Build configuration list */ = {
+		8BBFF0292F987EF700E8E144 /* Build configuration list for PBXNativeTarget "DomainDigCLI" */ = {
 			isa = XCConfigurationList;
 			buildConfigurations = (
 				8BBFF0272F987E8700E8E144 /* Debug */,
diff --git a/DomainDig/AppVersion.swift b/DomainDig/AppVersion.swift
new file mode 100644
index 0000000..a1516b8
--- /dev/null
+++ b/DomainDig/AppVersion.swift
@@ -0,0 +1,7 @@
+import Foundation
+
+enum AppVersion {
+    static var current: String {
+        "2.7.0"
+    }
+}
diff --git a/DomainDig/ContentView.swift b/DomainDig/ContentView.swift
index 21c37d2..ed2da49 100644
--- a/DomainDig/ContentView.swift
+++ b/DomainDig/ContentView.swift
@@ -42,15 +42,20 @@ struct ContentView: View {
                     }
                     if viewModel.hasRun {
                         actionButtons
-                        if let statusMessage = resultStatusMessage {
+                        if !viewModel.resultsLoaded {
+                            LookupProgressOverviewView(steps: viewModel.activeLoadingLabels)
+                                .padding(.top, appDensity.metrics.cardSpacing)
+                        } else if let statusMessage = resultStatusMessage {
                             LookupStatusBannerView(message: statusMessage, resultSource: viewModel.currentResultSource)
                                 .padding(.top, appDensity.metrics.cardSpacing)
                         }
-                        SummaryView(fields: viewModel.summaryFields)
-                            .padding(.top, appDensity.metrics.cardSpacing)
-                        if let changeSummary = viewModel.currentChangeSummary {
-                            DomainChangeSummaryView(summary: changeSummary)
+                        if viewModel.resultsLoaded {
+                            SummaryView(fields: viewModel.summaryFields)
                                 .padding(.top, appDensity.metrics.cardSpacing)
+                            if let changeSummary = viewModel.currentChangeSummary {
+                                DomainChangeSummaryView(summary: changeSummary)
+                                    .padding(.top, appDensity.metrics.cardSpacing)
+                            }
                         }
                         DomainSectionView(
                             isCollapsed: sectionCollapsedBinding(.domain),
@@ -59,6 +64,9 @@ struct ContentView: View {
                             showSuggestions: viewModel.availabilityResult?.status == .registered || viewModel.suggestionsLoading,
                             availabilityLoading: viewModel.availabilityLoading,
                             suggestionsLoading: viewModel.suggestionsLoading,
+                            provenance: viewModel.currentSnapshot.provenanceBySection[.availability],
+                            confidence: viewModel.currentSnapshot.availabilityConfidence,
+                            snapshotNote: viewModel.currentSnapshot.note,
                             trackedDomain: viewModel.currentTrackedDomain,
                             trackingLimitMessage: viewModel.trackingLimitMessage,
                             onTrack: {
@@ -82,6 +90,8 @@ struct ContentView: View {
                             rows: viewModel.ownershipRows,
                             loading: viewModel.ownershipLoading,
                             error: viewModel.ownershipError,
+                            provenance: viewModel.currentSnapshot.provenanceBySection[.ownership],
+                            confidence: viewModel.currentSnapshot.ownershipConfidence,
                             showsHistoryPlaceholder: !DataAccessService.hasAccess(to: .ownershipHistory)
                         )
                         .padding(.top, appDensity.metrics.sectionSpacing)
@@ -90,6 +100,8 @@ struct ContentView: View {
                             rows: viewModel.subdomainRows,
                             loading: viewModel.subdomainsLoading,
                             error: viewModel.subdomainsError,
+                            provenance: viewModel.currentSnapshot.provenanceBySection[.subdomains],
+                            confidence: viewModel.currentSnapshot.subdomainConfidence,
                             showsExtendedPlaceholder: !DataAccessService.hasAccess(to: .extendedSubdomains)
                         )
                         .padding(.top, appDensity.metrics.sectionSpacing)
@@ -97,6 +109,7 @@ struct ContentView: View {
                             DomainDiffView(
                                 title: "Latest Changes",
                                 sections: viewModel.currentDiffSections,
+                                contextNote: viewModel.currentChangeSummary?.contextNote,
                                 showsUnchanged: false
                             )
                             .padding(.top, appDensity.metrics.sectionSpacing)
@@ -107,6 +120,8 @@ struct ContentView: View {
                             sections: viewModel.dnsRows,
                             ptrMessage: viewModel.ptrMessage,
                             loading: viewModel.dnsLoading || viewModel.ptrLoading,
+                            dnsProvenance: viewModel.currentSnapshot.provenanceBySection[.dns],
+                            ptrProvenance: viewModel.currentSnapshot.provenanceBySection[.ptr],
                             sectionError: viewModel.dnsError
                         )
                         .padding(.top, appDensity.metrics.sectionSpacing)
@@ -116,13 +131,16 @@ struct ContentView: View {
                             sslInfo: viewModel.sslInfo,
                             sslLoading: viewModel.sslLoading || viewModel.hstsLoading,
                             sslError: viewModel.sslError,
+                            tlsProvenance: viewModel.currentSnapshot.provenanceBySection[.ssl],
                             responseRows: viewModel.webResponseRows,
                             headers: viewModel.httpHeaders,
                             headersLoading: viewModel.httpHeadersLoading,
                             headersError: viewModel.httpHeadersError,
+                            httpProvenance: viewModel.currentSnapshot.provenanceBySection[.httpHeaders],
                             redirects: viewModel.redirectRows,
                             redirectLoading: viewModel.redirectChainLoading,
                             redirectError: viewModel.redirectChainError,
+                            redirectProvenance: viewModel.currentSnapshot.provenanceBySection[.redirectChain],
                             finalURL: viewModel.currentSnapshot.redirectChain.last?.url
                         )
                         .padding(.top, appDensity.metrics.sectionSpacing)
@@ -130,6 +148,8 @@ struct ContentView: View {
                             isCollapsed: sectionCollapsedBinding(.email),
                             rows: viewModel.emailRows,
                             loading: viewModel.emailSecurityLoading,
+                            provenance: viewModel.currentSnapshot.provenanceBySection[.emailSecurity],
+                            confidence: viewModel.currentSnapshot.emailSecurityConfidence,
                             error: viewModel.emailSecurityError
                         )
                         .padding(.top, appDensity.metrics.sectionSpacing)
@@ -138,14 +158,18 @@ struct ContentView: View {
                             reachabilityRows: viewModel.reachabilityRows,
                             reachabilityLoading: viewModel.reachabilityLoading,
                             reachabilityError: viewModel.reachabilityError,
+                            reachabilityProvenance: viewModel.currentSnapshot.provenanceBySection[.reachability],
                             locationRows: viewModel.locationRows,
                             geolocation: viewModel.ipGeolocation,
                             geolocationLoading: viewModel.ipGeolocationLoading,
                             geolocationError: viewModel.ipGeolocationError,
+                            geolocationProvenance: viewModel.currentSnapshot.provenanceBySection[.ipGeolocation],
+                            geolocationConfidence: viewModel.currentSnapshot.geolocationConfidence,
                             standardPortRows: viewModel.standardPortRows,
                             customPortRows: viewModel.customPortRows,
                             portScanLoading: viewModel.portScanLoading,
                             portScanError: viewModel.portScanError,
+                            portScanProvenance: viewModel.currentSnapshot.provenanceBySection[.portScan],
                             customPortScanLoading: viewModel.customPortScanLoading,
                             customPortScanError: viewModel.customPortScanError,
                             isCloudflareProxied: viewModel.isCloudflareProxied,
@@ -161,27 +185,6 @@ struct ContentView: View {
                 .padding(.horizontal)
                 .padding(.bottom, 32)
             }
-            .safeAreaInset(edge: .top) {
-                if viewModel.hasRun {
-                    StickyLookupSummaryView(
-                        domain: viewModel.searchedDomain,
-                        availability: viewModel.availabilityResult?.status,
-                        primaryIP: currentPrimaryIP,
-                        sslInfo: viewModel.sslInfo,
-                        sslError: viewModel.sslError,
-                        emailSecurity: viewModel.emailSecurity,
-                        emailError: viewModel.emailSecurityError,
-                        changeSummary: viewModel.currentChangeSummary
-                    )
-                    .padding(.horizontal)
-                    .padding(.top, 6)
-                    .background {
-                        Rectangle()
-                            .fill(.ultraThinMaterial)
-                            .opacity(0.96)
-                    }
-                }
-            }
             .background(
                 LinearGradient(
                     colors: [Color.black, Color(.systemGray6).opacity(0.12)],
@@ -521,11 +524,7 @@ struct ContentView: View {
     }
 
     private var defaultCollapsedSections: Set<ResultSection> {
-        var sections: Set<ResultSection> = []
-        if viewModel.standardPortRows.count + viewModel.customPortRows.count > 6 || currentPrimaryIP == nil {
-            sections.insert(.network)
-        }
-        return sections
+        []
     }
 
     private var currentPrimaryIP: String? {
@@ -636,6 +635,29 @@ struct StickyLookupSummaryView: View {
     }
 }
 
+struct LookupProgressOverviewView: View {
+    @Environment(\.appDensity) private var appDensity
+    let steps: [String]
+
+    var body: some View {
+        CardView(allowsHorizontalScroll: false) {
+            HStack(spacing: 8) {
+                ProgressView()
+                    .controlSize(.small)
+                VStack(alignment: .leading, spacing: 4) {
+                    Text("Running lookup…")
+                        .font(appDensity.font(.caption))
+                        .foregroundStyle(.primary)
+                    Text(steps.isEmpty ? "Preparing requests" : steps.joined(separator: " • "))
+                        .font(appDensity.font(.caption2))
+                        .foregroundStyle(.secondary)
+                }
+                Spacer()
+            }
+        }
+    }
+}
+
 struct LookupStatusBannerView: View {
     @Environment(\.appDensity) private var appDensity
     let message: String
@@ -686,6 +708,7 @@ struct LookupStatusBannerView: View {
 struct DomainChangeSummaryView: View {
     @Environment(\.appDensity) private var appDensity
     let summary: DomainChangeSummary
+    @State private var showsDetails = false
 
     var body: some View {
         CardView(allowsHorizontalScroll: false) {
@@ -706,9 +729,43 @@ struct DomainChangeSummaryView: View {
                     .foregroundStyle(.secondary)
             }
 
-            Text(summary.message)
+            VStack(alignment: .leading, spacing: 4) {
+                Text("Inference")
+                    .font(appDensity.font(.caption2))
+                    .foregroundStyle(.secondary)
+                Text(summary.message)
+                    .font(appDensity.font(.caption))
+                    .foregroundStyle(.primary)
+                    .lineLimit(2)
+            }
+
+            if !summary.observedFacts.isEmpty || summary.contextNote != nil {
+                DisclosureGroup(showsDetails ? "Hide Details" : "Show Details", isExpanded: $showsDetails) {
+                    VStack(alignment: .leading, spacing: 8) {
+                        if !summary.observedFacts.isEmpty {
+                            VStack(alignment: .leading, spacing: 4) {
+                                Text("Observed")
+                                    .font(appDensity.font(.caption2))
+                                    .foregroundStyle(.secondary)
+                                ForEach(Array(summary.observedFacts.enumerated()), id: \.offset) { _, fact in
+                                    Text(fact)
+                                        .font(appDensity.font(.caption))
+                                        .foregroundStyle(.primary)
+                                }
+                            }
+                        }
+
+                        if let contextNote = summary.contextNote {
+                            Text(contextNote)
+                                .font(appDensity.font(.caption2))
+                                .foregroundStyle(.orange)
+                        }
+                    }
+                    .padding(.top, 4)
+                }
                 .font(appDensity.font(.caption))
-                .foregroundStyle(.primary)
+                .tint(.secondary)
+            }
         }
     }
 
@@ -727,6 +784,7 @@ struct DomainChangeSummaryView: View {
 struct DomainDiffView: View {
     let title: String
     let sections: [DomainDiffSection]
+    let contextNote: String?
     let showsUnchanged: Bool
 
     @State private var collapsedSections = Set<UUID>()
@@ -766,6 +824,9 @@ struct DomainDiffView: View {
                     .font(.system(.caption, design: .monospaced))
                 }
             }
+            if let contextNote {
+                MessageCardView(text: contextNote, isError: false)
+            }
             if filteredSections.isEmpty {
                 MessageCardView(text: "No comparison data available", isError: false)
             } else {
@@ -917,6 +978,9 @@ struct DomainSectionView: View {
     let showSuggestions: Bool
     let availabilityLoading: Bool
     let suggestionsLoading: Bool
+    let provenance: SectionProvenance?
+    let confidence: ConfidenceLevel?
+    let snapshotNote: String?
     let trackedDomain: TrackedDomain?
     let trackingLimitMessage: String?
     let onTrack: () -> Void
@@ -953,6 +1017,11 @@ struct DomainSectionView: View {
             }
         } content: {
             CardView(allowsHorizontalScroll: false) {
+                SectionTrustMetadataView(
+                    provenance: provenance,
+                    confidence: confidence,
+                    note: snapshotNote == nil ? nil : "Audit note present"
+                )
                 ForEach(rows) { row in
                     LabeledValueRow(row: row)
                 }
@@ -986,7 +1055,7 @@ struct DomainSectionView: View {
                                     .foregroundStyle(.primary)
                                     .textSelection(.enabled)
                                 Spacer()
-                                AppStatusBadgeView(model: AppStatusFactory.availability(suggestion.status == "Available" ? .available : .registered))
+                                AppStatusBadgeView(model: AppStatusFactory.availability(suggestion.availabilityStatus))
                             }
                         }
                     }
@@ -1001,11 +1070,14 @@ struct OwnershipSectionView: View {
     let rows: [InfoRowViewData]
     let loading: Bool
     let error: String?
+    let provenance: SectionProvenance?
+    let confidence: ConfidenceLevel?
     let showsHistoryPlaceholder: Bool
 
     var body: some View {
         CollapsibleSectionView(title: "Ownership", isCollapsed: $isCollapsed) {
             CardView(allowsHorizontalScroll: false) {
+                SectionTrustMetadataView(provenance: provenance, confidence: confidence)
                 if loading {
                     ProgressView("Fetching RDAP ownership…")
                         .appLoadingStyle()
@@ -1033,11 +1105,14 @@ struct SubdomainsSectionView: View {
     let rows: [SubdomainRowViewData]
     let loading: Bool
     let error: String?
+    let provenance: SectionProvenance?
+    let confidence: ConfidenceLevel?
     let showsExtendedPlaceholder: Bool
 
     var body: some View {
         CollapsibleSectionView(title: "Subdomains", isCollapsed: $isCollapsed, subtitle: "\(rows.count) found") {
             CardView(allowsHorizontalScroll: false) {
+                SectionTrustMetadataView(provenance: provenance, confidence: confidence)
                 if loading {
                     ProgressView("Checking certificate transparency…")
                         .appLoadingStyle()
@@ -1082,6 +1157,8 @@ struct DNSSectionView: View {
     let sections: [DNSRecordSectionViewData]
     let ptrMessage: SectionMessageViewData?
     let loading: Bool
+    let dnsProvenance: SectionProvenance?
+    let ptrProvenance: SectionProvenance?
     let sectionError: String?
 
     var body: some View {
@@ -1091,6 +1168,11 @@ struct DNSSectionView: View {
             } else if let sectionError, sections.isEmpty {
                 MessageCardView(text: sectionError, isError: true)
             } else {
+                if dnsProvenance != nil {
+                    CardView(allowsHorizontalScroll: false) {
+                        SectionTrustMetadataView(provenance: dnsProvenance, confidence: nil)
+                    }
+                }
                 ForEach(sections) { section in
                     CardView {
                         Text(section.title)
@@ -1124,6 +1206,7 @@ struct DNSSectionView: View {
                             .font(.system(.subheadline, design: .monospaced))
                             .fontWeight(.semibold)
                             .foregroundStyle(.cyan)
+                        SectionTrustMetadataView(provenance: ptrProvenance, confidence: nil)
                         MessageRowView(text: ptrMessage.text, isError: ptrMessage.isError)
                     }
                 }
@@ -1139,13 +1222,16 @@ struct WebSectionView: View {
     let sslInfo: SSLCertificateInfo?
     let sslLoading: Bool
     let sslError: String?
+    let tlsProvenance: SectionProvenance?
     let responseRows: [InfoRowViewData]
     let headers: [HTTPHeader]
     let headersLoading: Bool
     let headersError: String?
+    let httpProvenance: SectionProvenance?
     let redirects: [RedirectHopViewData]
     let redirectLoading: Bool
     let redirectError: String?
+    let redirectProvenance: SectionProvenance?
     let finalURL: String?
 
     var body: some View {
@@ -1158,6 +1244,7 @@ struct WebSectionView: View {
                     Spacer()
                     AppStatusBadgeView(model: AppStatusFactory.tls(sslInfo: sslInfo, error: sslError))
                 }
+                SectionTrustMetadataView(provenance: tlsProvenance, confidence: nil)
                 if sslLoading {
                     ProgressView("Checking certificate…")
                         .appLoadingStyle()
@@ -1188,6 +1275,7 @@ struct WebSectionView: View {
                 Text("Headers")
                     .font(appDensity.font(.subheadline, weight: .semibold))
                     .foregroundStyle(.cyan)
+                SectionTrustMetadataView(provenance: httpProvenance, confidence: nil)
                 if headersLoading {
                     ProgressView("Fetching headers…")
                         .appLoadingStyle()
@@ -1225,6 +1313,7 @@ struct WebSectionView: View {
                         AppCopyButton(value: finalURL, label: "Copy redirect URL")
                     }
                 }
+                SectionTrustMetadataView(provenance: redirectProvenance, confidence: nil)
                 if redirectLoading {
                     ProgressView("Tracing redirects…")
                         .appLoadingStyle()
@@ -1268,11 +1357,14 @@ struct EmailSectionView: View {
     @Binding var isCollapsed: Bool
     let rows: [EmailRowViewData]
     let loading: Bool
+    let provenance: SectionProvenance?
+    let confidence: ConfidenceLevel?
     let error: String?
 
     var body: some View {
         CollapsibleSectionView(title: "Email", isCollapsed: $isCollapsed) {
             CardView {
+                SectionTrustMetadataView(provenance: provenance, confidence: confidence)
                 HStack {
                     Spacer()
                     AppStatusBadgeView(model: AppStatusFactory.email(nil, error: error))
@@ -1331,14 +1423,18 @@ struct NetworkSectionView: View {
     let reachabilityRows: [ReachabilityRowViewData]
     let reachabilityLoading: Bool
     let reachabilityError: String?
+    let reachabilityProvenance: SectionProvenance?
     let locationRows: [InfoRowViewData]
     let geolocation: IPGeolocation?
     let geolocationLoading: Bool
     let geolocationError: String?
+    let geolocationProvenance: SectionProvenance?
+    let geolocationConfidence: ConfidenceLevel?
     let standardPortRows: [PortScanRowViewData]
     let customPortRows: [PortScanRowViewData]
     let portScanLoading: Bool
     let portScanError: String?
+    let portScanProvenance: SectionProvenance?
     let customPortScanLoading: Bool
     let customPortScanError: String?
     let isCloudflareProxied: Bool
@@ -1352,6 +1448,7 @@ struct NetworkSectionView: View {
                 Text("Reachability")
                     .font(appDensity.font(.subheadline, weight: .semibold))
                     .foregroundStyle(.cyan)
+                SectionTrustMetadataView(provenance: reachabilityProvenance, confidence: nil)
                 if reachabilityLoading {
                     ProgressView("Checking ports…")
                         .appLoadingStyle()
@@ -1376,6 +1473,7 @@ struct NetworkSectionView: View {
                 Text("Location")
                     .font(appDensity.font(.subheadline, weight: .semibold))
                     .foregroundStyle(.cyan)
+                SectionTrustMetadataView(provenance: geolocationProvenance, confidence: geolocationConfidence)
                 if geolocationLoading {
                     ProgressView("Looking up location…")
                         .appLoadingStyle()
@@ -1407,6 +1505,7 @@ struct NetworkSectionView: View {
                 Text("Port Scan")
                     .font(appDensity.font(.subheadline, weight: .semibold))
                     .foregroundStyle(.cyan)
+                SectionTrustMetadataView(provenance: portScanProvenance, confidence: nil)
 
                 if isCloudflareProxied {
                     Text("Domain is behind Cloudflare's proxy. Results reflect the edge, not the origin.")
@@ -1609,6 +1708,62 @@ struct MessageRowView: View {
     }
 }
 
+struct SectionTrustMetadataView: View {
+    @Environment(\.appDensity) private var appDensity
+    let provenance: SectionProvenance?
+    let confidence: ConfidenceLevel?
+    let note: String?
+
+    init(provenance: SectionProvenance?, confidence: ConfidenceLevel?, note: String? = nil) {
+        self.provenance = provenance
+        self.confidence = confidence
+        self.note = note
+    }
+
+    var body: some View {
+        if provenance != nil || confidence != nil || note != nil {
+            VStack(alignment: .leading, spacing: 6) {
+                HStack(spacing: 8) {
+                    if let confidence {
+                        Text("Confidence \(confidence.title)")
+                            .font(appDensity.font(.caption2))
+                            .foregroundStyle(.secondary)
+                    }
+                    if let provenance {
+                        Text(provenance.provider ?? provenance.source)
+                            .font(appDensity.font(.caption2))
+                            .foregroundStyle(.secondary)
+                        Text(provenance.resultSource.label)
+                            .font(appDensity.font(.caption2))
+                            .foregroundStyle(.secondary)
+                    }
+                }
+                DisclosureGroup("Details") {
+                    VStack(alignment: .leading, spacing: 4) {
+                        if let provenance {
+                            LabeledValueRow(row: .init(label: "Method", value: provenance.source, tone: .secondary))
+                            if let provider = provenance.provider {
+                                LabeledValueRow(row: .init(label: "Provider", value: provider, tone: .secondary))
+                            }
+                            if let resolver = provenance.resolver {
+                                LabeledValueRow(row: .init(label: "Resolver", value: resolver, tone: .secondary))
+                            }
+                            LabeledValueRow(row: .init(label: "Collected", value: provenance.collectedAt.formatted(date: .abbreviated, time: .shortened), tone: .secondary))
+                            LabeledValueRow(row: .init(label: "Mode", value: provenance.resultSource.label, tone: .secondary))
+                        }
+                        if let note {
+                            LabeledValueRow(row: .init(label: "Note", value: note, tone: .secondary))
+                        }
+                    }
+                    .padding(.top, 4)
+                }
+                .font(appDensity.font(.caption))
+                .tint(.secondary)
+            }
+        }
+    }
+}
+
 struct LabeledValueRow: View {
     @Environment(\.appDensity) private var appDensity
     let row: InfoRowViewData
@@ -1737,7 +1892,6 @@ private struct SettingsView: View {
             Section("About") {
                 LabeledContent("Version", value: appVersion)
                 LabeledContent("Storage", value: "Local-only")
-                LabeledContent("Focus", value: "Readable domain inspection")
             }
         }
         .navigationTitle("Settings")
@@ -1776,7 +1930,7 @@ private struct SettingsView: View {
     }
 
     private var appVersion: String {
-        Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "2.6.0"
+        AppVersion.current
     }
 }
 
diff --git a/DomainDig/DomainAvailabilityService.swift b/DomainDig/DomainAvailabilityService.swift
index 9a350ef..59f6afa 100644
--- a/DomainDig/DomainAvailabilityService.swift
+++ b/DomainDig/DomainAvailabilityService.swift
@@ -15,8 +15,12 @@ struct DomainAvailabilityService {
         }
 
         let fallbackStatus = await checkViaDNSFallback(domain: normalizedDomain)
-        let method = fallbackStatus == .registered ? "dns" : "fallback"
-        debugLog(method, domain: normalizedDomain, status: fallbackStatus)
+        if fallbackStatus == .registered {
+            debugLog("dns-evidence", domain: normalizedDomain, details: "DNS exists but RDAP did not confirm registration")
+            return DomainAvailabilityResult(domain: normalizedDomain, status: .unknown)
+        }
+
+        debugLog("fallback", domain: normalizedDomain, status: fallbackStatus)
         return DomainAvailabilityResult(domain: normalizedDomain, status: fallbackStatus)
     }
 
diff --git a/DomainDig/DomainDiffService.swift b/DomainDig/DomainDiffService.swift
index 340873d..528841d 100644
--- a/DomainDig/DomainDiffService.swift
+++ b/DomainDig/DomainDiffService.swift
@@ -67,16 +67,33 @@ enum DomainDiffService {
         let highlights = summaryHighlights(from: allChangedItems)
         let severity = allChangedItems.map(\.severity).max() ?? .low
         let message = summaryMessage(from: allChangedItems, highlights: highlights)
+        let observedFacts = observedFacts(from: allChangedItems)
+        let inferredConclusions = highlights.isEmpty ? [] : [message]
+        let contextNote = comparisonContextNote(from: oldSnapshot, to: newSnapshot)
 
         return DomainChangeSummary(
             hasChanges: !allChangedItems.isEmpty,
             changedSections: highlights,
             message: message,
             severity: severity,
-            generatedAt: generatedAt
+            generatedAt: generatedAt,
+            observedFacts: observedFacts,
+            inferredConclusions: inferredConclusions,
+            contextNote: contextNote
         )
     }
 
+    static func comparisonContextNote(from oldSnapshot: LookupSnapshot, to newSnapshot: LookupSnapshot) -> String? {
+        var notes: [String] = []
+        if oldSnapshot.resolverURLString != newSnapshot.resolverURLString {
+            notes.append("Compared snapshots used different DNS resolvers.")
+        }
+        if oldSnapshot.resultSource != newSnapshot.resultSource {
+            notes.append("Compared snapshots came from different collection modes.")
+        }
+        return notes.isEmpty ? nil : notes.joined(separator: " ")
+    }
+
     static func certificateWarningLevel(for snapshot: LookupSnapshot) -> CertificateWarningLevel {
         guard let days = snapshot.sslInfo?.daysUntilExpiry else {
             return .none
@@ -410,6 +427,14 @@ enum DomainDiffService {
         return "\(highlights[0]) and \(highlights[1].lowercased())"
     }
 
+    private static func observedFacts(from items: [DomainDiffItem]) -> [String] {
+        items.prefix(3).map { item in
+            let oldValue = item.oldValue ?? "none"
+            let newValue = item.newValue ?? "none"
+            return "\(item.label): \(oldValue) -> \(newValue)"
+        }
+    }
+
     private static func normalized(_ value: String?) -> String? {
         guard let value = value?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else {
             return nil
diff --git a/DomainDig/DomainViewModel.swift b/DomainDig/DomainViewModel.swift
index 326db3a..3373447 100644
--- a/DomainDig/DomainViewModel.swift
+++ b/DomainDig/DomainViewModel.swift
@@ -87,6 +87,7 @@ struct SubdomainRowViewData: Identifiable {
 struct DomainSuggestionViewData: Identifiable {
     let id: UUID
     let domain: String
+    let availabilityStatus: DomainAvailabilityStatus
     let status: String
     let tone: ResultTone
 }
@@ -204,13 +205,7 @@ final class DomainViewModel {
 
     private static let historyKey = "lookupHistory"
     private static let maxHistory = 250
-    var history: [HistoryEntry] = {
-        guard let data = UserDefaults.standard.data(forKey: historyKey),
-              let entries = try? JSONDecoder().decode([HistoryEntry].self, from: data) else {
-            return []
-        }
-        return entries
-    }()
+    var history: [HistoryEntry] = DomainViewModel.loadHistoryEntries()
     var historySearchText = ""
     var historyDateFilter: HistoryDateFilter = .all
     var historyChangeFilter: ChangeFilterOption = .all
@@ -246,6 +241,24 @@ final class DomainViewModel {
             !customPortScanLoading
     }
 
+    var activeLoadingLabels: [String] {
+        var labels: [String] = []
+        if availabilityLoading { labels.append("Availability") }
+        if dnsLoading { labels.append("DNS") }
+        if sslLoading || hstsLoading { labels.append("TLS") }
+        if httpHeadersLoading { labels.append("HTTP") }
+        if ownershipLoading { labels.append("Ownership") }
+        if emailSecurityLoading { labels.append("Email") }
+        if subdomainsLoading { labels.append("Subdomains") }
+        if redirectChainLoading { labels.append("Redirects") }
+        if reachabilityLoading { labels.append("Reachability") }
+        if ipGeolocationLoading { labels.append("Geolocation") }
+        if ptrLoading { labels.append("PTR") }
+        if portScanLoading { labels.append("Port Scan") }
+        if customPortScanLoading { labels.append("Custom Ports") }
+        return labels
+    }
+
     var isCloudflareProxied: Bool {
         httpHeaders.contains { $0.name.lowercased() == "cf-ray" }
     }
@@ -365,8 +378,20 @@ final class DomainViewModel {
             domain: searchedDomain,
             timestamp: currentSnapshotTimestamp,
             trackedDomainID: currentTrackedDomain?.id,
+            note: currentHistoryEntry?.note ?? currentTrackedDomain?.note,
+            appVersion: AppVersion.current,
             resolverDisplayName: resolverDisplayName,
             resolverURLString: resolverURLString,
+            dataSources: currentHistoryEntry?.dataSources ?? [],
+            provenanceBySection: currentHistoryEntry?.provenanceBySection ?? [:],
+            availabilityConfidence: currentHistoryEntry?.availabilityConfidence,
+            ownershipConfidence: currentHistoryEntry?.ownershipConfidence,
+            subdomainConfidence: currentHistoryEntry?.subdomainConfidence,
+            emailSecurityConfidence: currentHistoryEntry?.emailSecurityConfidence,
+            geolocationConfidence: currentHistoryEntry?.geolocationConfidence,
+            errorDetails: currentHistoryEntry?.errorDetails ?? [:],
+            isPartialSnapshot: currentHistoryEntry?.isPartialSnapshot ?? false,
+            validationIssues: currentHistoryEntry?.validationIssues ?? [],
             totalLookupDurationMs: lastLookupDurationMs,
             dnsSections: dnsSections,
             dnsError: dnsError,
@@ -405,6 +430,11 @@ final class DomainViewModel {
         )
     }
 
+    private var currentHistoryEntry: HistoryEntry? {
+        guard let currentHistoryEntryID else { return nil }
+        return history.first(where: { $0.id == currentHistoryEntryID })
+    }
+
     var currentReport: DomainReport? {
         guard !searchedDomain.isEmpty else { return nil }
         return reportBuilder.build(
@@ -543,9 +573,7 @@ final class DomainViewModel {
     }
 
     func rerunInspection(for trackedDomain: TrackedDomain) {
-        domain = trackedDomain.domain
-        run()
-        rerunNavigationToken = UUID()
+        rerunInspection(for: trackedDomain, useSnapshotResolver: false)
     }
 
     func deleteTrackedDomains(at offsets: IndexSet) {
@@ -609,13 +637,24 @@ final class DomainViewModel {
         UserDefaults.standard.removeObject(forKey: Self.recentSearchesKey)
     }
 
-    func rerunLookup(from entry: HistoryEntry) {
-        UserDefaults.standard.set(entry.resolverURLString, forKey: DNSResolverOption.userDefaultsKey)
+    func rerunLookup(from entry: HistoryEntry, useSnapshotResolver: Bool) {
+        if useSnapshotResolver {
+            UserDefaults.standard.set(entry.resolverURLString, forKey: DNSResolverOption.userDefaultsKey)
+        }
         domain = entry.domain
         run()
         rerunNavigationToken = UUID()
     }
 
+    func rerunInspection(for trackedDomain: TrackedDomain, useSnapshotResolver: Bool) {
+        if useSnapshotResolver, let snapshot = latestSnapshot(for: trackedDomain) {
+            UserDefaults.standard.set(snapshot.resolverURLString, forKey: DNSResolverOption.userDefaultsKey)
+        }
+        domain = trackedDomain.domain
+        run()
+        rerunNavigationToken = UUID()
+    }
+
     func reset() {
         lookupTask?.cancel()
         customPortScanTask?.cancel()
@@ -853,8 +892,20 @@ final class DomainViewModel {
             domain: previousSnapshot.domain,
             timestamp: previousSnapshot.timestamp,
             trackedDomainID: previousSnapshot.trackedDomainID,
+            note: previousSnapshot.note,
+            appVersion: previousSnapshot.appVersion,
             resolverDisplayName: previousSnapshot.resolverDisplayName,
             resolverURLString: previousSnapshot.resolverURLString,
+            dataSources: previousSnapshot.dataSources,
+            provenanceBySection: previousSnapshot.provenanceBySection,
+            availabilityConfidence: previousSnapshot.availabilityConfidence,
+            ownershipConfidence: previousSnapshot.ownershipConfidence,
+            subdomainConfidence: previousSnapshot.subdomainConfidence,
+            emailSecurityConfidence: previousSnapshot.emailSecurityConfidence,
+            geolocationConfidence: previousSnapshot.geolocationConfidence,
+            errorDetails: previousSnapshot.errorDetails,
+            isPartialSnapshot: previousSnapshot.isPartialSnapshot,
+            validationIssues: previousSnapshot.validationIssues,
             totalLookupDurationMs: previousSnapshot.totalLookupDurationMs,
             dnsSections: previousSnapshot.dnsSections,
             dnsError: previousSnapshot.dnsError,
@@ -1232,6 +1283,7 @@ final class DomainViewModel {
             domain: snapshot.domain,
             timestamp: snapshot.timestamp,
             trackedDomainID: trackedDomainID,
+            note: currentHistoryEntry?.note,
             dnsSections: snapshot.dnsSections,
             sslInfo: snapshot.sslInfo,
             httpHeaders: snapshot.httpHeaders,
@@ -1247,6 +1299,18 @@ final class DomainViewModel {
             hstsPreloaded: snapshot.hstsPreloaded,
             availabilityResult: snapshot.availabilityResult,
             suggestions: snapshot.suggestions,
+            appVersion: snapshot.appVersion,
+            resultSource: snapshot.resultSource,
+            dataSources: snapshot.dataSources,
+            provenanceBySection: snapshot.provenanceBySection,
+            availabilityConfidence: snapshot.availabilityConfidence,
+            ownershipConfidence: snapshot.ownershipConfidence,
+            subdomainConfidence: snapshot.subdomainConfidence,
+            emailSecurityConfidence: snapshot.emailSecurityConfidence,
+            geolocationConfidence: snapshot.geolocationConfidence,
+            errorDetails: snapshot.errorDetails,
+            isPartialSnapshot: snapshot.isPartialSnapshot,
+            validationIssues: snapshot.validationIssues,
             resolverDisplayName: snapshot.resolverDisplayName,
             resolverURLString: snapshot.resolverURLString,
             totalLookupDurationMs: snapshot.totalLookupDurationMs,
@@ -1302,6 +1366,12 @@ final class DomainViewModel {
         }
     }
 
+    func updateHistoryNote(_ note: String, for entry: HistoryEntry) {
+        guard let index = history.firstIndex(where: { $0.id == entry.id }) else { return }
+        history[index].note = note.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty
+        persistHistory()
+    }
+
     private func persistTrackedDomains() {
         if let data = try? JSONEncoder().encode(trackedDomains) {
             UserDefaults.standard.set(data, forKey: Self.trackedDomainsKey)
@@ -1777,6 +1847,22 @@ final class DomainViewModel {
         trackedDomain.lastChangeSummary ?? recentSnapshots(for: trackedDomain, limit: 1).first?.changeSummary
     }
 
+    func latestSnapshot(for trackedDomain: TrackedDomain) -> LookupSnapshot? {
+        recentSnapshots(for: trackedDomain, limit: 1).first?.snapshot
+    }
+
+    func resolverMismatchNote(for entry: HistoryEntry) -> String? {
+        guard entry.resolverURLString != resolverURLString else { return nil }
+        return "Current resolver differs from this snapshot. Re-running may produce different evidence."
+    }
+
+    func resolverMismatchNote(for trackedDomain: TrackedDomain) -> String? {
+        guard let snapshot = latestSnapshot(for: trackedDomain), snapshot.resolverURLString != resolverURLString else {
+            return nil
+        }
+        return "Current resolver differs from the latest snapshot for this tracked domain."
+    }
+
     func comparisonSnapshot(for entry: HistoryEntry) -> LookupSnapshot? {
         let siblings = history.filter { candidate in
             if let trackedDomainID = entry.trackedDomainID {
@@ -1834,8 +1920,20 @@ final class DomainViewModel {
             domain: trackedDomain.domain,
             timestamp: trackedDomain.updatedAt,
             trackedDomainID: trackedDomain.id,
+            note: trackedDomain.note,
+            appVersion: AppVersion.current,
             resolverDisplayName: resolverDisplayName,
             resolverURLString: resolverURLString,
+            dataSources: [],
+            provenanceBySection: [:],
+            availabilityConfidence: nil,
+            ownershipConfidence: nil,
+            subdomainConfidence: nil,
+            emailSecurityConfidence: nil,
+            geolocationConfidence: nil,
+            errorDetails: [:],
+            isPartialSnapshot: true,
+            validationIssues: ["No stored snapshot data available"],
             totalLookupDurationMs: nil,
             dnsSections: [],
             dnsError: nil,
@@ -1874,6 +1972,31 @@ final class DomainViewModel {
         )
     }
 
+    private static func loadHistoryEntries() -> [HistoryEntry] {
+        let defaults = UserDefaults.standard
+        guard let data = defaults.data(forKey: historyKey) else {
+            return []
+        }
+
+        if let entries = try? JSONDecoder().decode([HistoryEntry].self, from: data) {
+            return entries
+        }
+
+        guard let rawArray = (try? JSONSerialization.jsonObject(with: data)) as? [Any] else {
+            return []
+        }
+
+        let decoder = JSONDecoder()
+        return rawArray.compactMap { item in
+            guard JSONSerialization.isValidJSONObject(item),
+                  let itemData = try? JSONSerialization.data(withJSONObject: item),
+                  let entry = try? decoder.decode(HistoryEntry.self, from: itemData) else {
+                return nil
+            }
+            return entry
+        }
+    }
+
     private static func loadTrackedDomains() -> [TrackedDomain] {
         let defaults = UserDefaults.standard
         let decoder = JSONDecoder()
@@ -1953,10 +2076,11 @@ final class DomainViewModel {
     static func summaryFields(from snapshot: LookupSnapshot) -> [SummaryFieldViewData] {
         [
             SummaryFieldViewData(label: "Domain", value: snapshot.domain.nonEmpty ?? "Unavailable", tone: .primary),
-            SummaryFieldViewData(label: "Primary IP", value: primaryIPAddress(from: snapshot) ?? "Unavailable", tone: .primary),
-            SummaryFieldViewData(label: "HTTPS", value: httpsSummary(from: snapshot), tone: httpsSummaryTone(from: snapshot)),
+            SummaryFieldViewData(label: "Observed IP", value: primaryIPAddress(from: snapshot) ?? "Unavailable", tone: .primary),
+            SummaryFieldViewData(label: "Observed Redirect", value: finalRedirectTarget(from: snapshot) ?? "Unavailable", tone: .secondary),
+            SummaryFieldViewData(label: "Inference", value: availabilityInference(from: snapshot), tone: availabilityTone(snapshot.availabilityResult?.status)),
+            SummaryFieldViewData(label: "Observed TLS", value: httpsSummary(from: snapshot), tone: httpsSummaryTone(from: snapshot)),
             SummaryFieldViewData(label: "Certificate", value: certificateStatusLabel(from: snapshot), tone: certificateStatusTone(from: snapshot)),
-            SummaryFieldViewData(label: "Redirect", value: finalRedirectTarget(from: snapshot) ?? "Unavailable", tone: .secondary),
             SummaryFieldViewData(label: "Source", value: snapshot.statusMessage ?? snapshot.resultSource.label, tone: sourceTone(for: snapshot))
         ]
     }
@@ -1965,17 +2089,32 @@ final class DomainViewModel {
         var rows = [
             InfoRowViewData(label: "Domain", value: snapshot.domain, tone: .primary),
             InfoRowViewData(label: "Resolver", value: snapshot.resolverDisplayName, tone: .secondary),
+            InfoRowViewData(label: "Collected", value: snapshot.timestamp.formatted(date: .abbreviated, time: .shortened), tone: .secondary),
             InfoRowViewData(label: snapshot.statusMessage == nil ? "Result" : "Snapshot", value: snapshot.statusMessage ?? snapshot.resultSource.label, tone: sourceTone(for: snapshot)),
             InfoRowViewData(label: "Lookup Duration", value: durationLabel(snapshot.totalLookupDurationMs), tone: .secondary)
         ]
         rows.insert(
             InfoRowViewData(
-                label: "Availability",
-                value: availabilityLabel(snapshot.availabilityResult?.status),
-                tone: availabilityTone(snapshot.availabilityResult?.status)
+                label: "Observed Availability",
+                value: snapshot.availabilityResult?.status == .unknown ? "No direct registration proof" : "Status collected",
+                tone: .secondary
             ),
             at: 1
         )
+        rows.insert(
+            InfoRowViewData(
+                label: "Inference",
+                value: availabilityInference(from: snapshot),
+                tone: availabilityTone(snapshot.availabilityResult?.status)
+            ),
+            at: 2
+        )
+        if let confidence = snapshot.availabilityConfidence {
+            rows.insert(
+                InfoRowViewData(label: "Confidence", value: confidence.title, tone: .secondary),
+                at: 3
+            )
+        }
         if let certificateStatus = certificateBadgeLabel(from: snapshot) {
             rows.insert(
                 InfoRowViewData(
@@ -1994,6 +2133,7 @@ final class DomainViewModel {
             DomainSuggestionViewData(
                 id: $0.id,
                 domain: $0.domain,
+                availabilityStatus: $0.status,
                 status: availabilityLabel($0.status),
                 tone: availabilityTone($0.status)
             )
@@ -2562,6 +2702,17 @@ final class DomainViewModel {
         }
     }
 
+    private static func availabilityInference(from snapshot: LookupSnapshot) -> String {
+        switch snapshot.availabilityResult?.status {
+        case .registered:
+            return "Likely registered"
+        case .available:
+            return "Possibly available"
+        case .unknown, .none:
+            return "Unclear"
+        }
+    }
+
     private static func availabilityTone(_ status: DomainAvailabilityStatus?) -> ResultTone {
         switch status {
         case .available:
diff --git a/DomainDig/HistoryView.swift b/DomainDig/HistoryView.swift
index 1a5c0d0..9505c12 100644
--- a/DomainDig/HistoryView.swift
+++ b/DomainDig/HistoryView.swift
@@ -39,6 +39,9 @@ struct HistoryView: View {
                                     HStack(spacing: 8) {
                                         AppStatusBadgeView(model: AppStatusFactory.availability(entry.availabilityResult?.status))
                                         AppStatusBadgeView(model: AppStatusFactory.tls(sslInfo: entry.sslInfo, error: entry.sslError))
+                                        if entry.isPartialSnapshot {
+                                            AppStatusBadgeView(model: .init(title: "Partial", systemImage: "exclamationmark.triangle.fill", foregroundColor: .yellow, backgroundColor: .yellow.opacity(0.16)))
+                                        }
                                     }
 
                                     HStack(spacing: 8) {
@@ -51,6 +54,13 @@ struct HistoryView: View {
                                     }
                                     .font(appDensity.font(.caption2))
                                     .foregroundStyle(.secondary)
+
+                                    if let note = entry.note, !note.isEmpty {
+                                        Text(note)
+                                            .font(appDensity.font(.caption2))
+                                            .foregroundStyle(.secondary)
+                                            .lineLimit(1)
+                                    }
                                 }
                             }
                             .swipeActions(edge: .trailing, allowsFullSwipe: true) {
@@ -128,6 +138,9 @@ struct HistoryDetailView: View {
     @Bindable var viewModel: DomainViewModel
     let entry: HistoryEntry
     @Environment(\.dismiss) private var dismiss
+    @State private var noteDraft = ""
+    @State private var isEditingNote = false
+    @State private var showRerunOptions = false
 
     private let dateFormatter: DateFormatter = {
         let formatter = DateFormatter()
@@ -153,6 +166,9 @@ struct HistoryDetailView: View {
                     showSuggestions: entry.availabilityResult?.status == .registered && !entry.suggestions.isEmpty,
                     availabilityLoading: false,
                     suggestionsLoading: false,
+                    provenance: snapshot.provenanceBySection[.availability],
+                    confidence: snapshot.availabilityConfidence,
+                    snapshotNote: entry.note,
                     trackedDomain: viewModel.trackedDomains.first(where: { $0.domain.lowercased() == entry.domain.lowercased() }),
                     trackingLimitMessage: nil,
                     onTrack: {
@@ -170,6 +186,8 @@ struct HistoryDetailView: View {
                     rows: DomainViewModel.ownershipRows(from: snapshot),
                     loading: false,
                     error: snapshot.ownershipError,
+                    provenance: snapshot.provenanceBySection[.ownership],
+                    confidence: snapshot.ownershipConfidence,
                     showsHistoryPlaceholder: !DataAccessService.hasAccess(to: .ownershipHistory)
                 )
                 .padding(.top, appDensity.metrics.sectionSpacing)
@@ -178,6 +196,8 @@ struct HistoryDetailView: View {
                     rows: DomainViewModel.subdomainRows(from: snapshot),
                     loading: false,
                     error: snapshot.subdomainsError,
+                    provenance: snapshot.provenanceBySection[.subdomains],
+                    confidence: snapshot.subdomainConfidence,
                     showsExtendedPlaceholder: !DataAccessService.hasAccess(to: .extendedSubdomains)
                 )
                 .padding(.top, appDensity.metrics.sectionSpacing)
@@ -189,6 +209,7 @@ struct HistoryDetailView: View {
                     DomainDiffView(
                         title: "Compared With Previous Snapshot",
                         sections: DomainDiffService.diff(from: comparisonSnapshot, to: snapshot),
+                        contextNote: DomainDiffService.comparisonContextNote(from: comparisonSnapshot, to: snapshot),
                         showsUnchanged: false
                     )
                     .padding(.top, appDensity.metrics.sectionSpacing)
@@ -199,6 +220,8 @@ struct HistoryDetailView: View {
                     sections: DomainViewModel.dnsRows(from: snapshot),
                     ptrMessage: DomainViewModel.ptrMessage(from: snapshot),
                     loading: false,
+                    dnsProvenance: snapshot.provenanceBySection[.dns],
+                    ptrProvenance: snapshot.provenanceBySection[.ptr],
                     sectionError: snapshot.dnsError
                 )
                 .padding(.top, appDensity.metrics.sectionSpacing)
@@ -208,13 +231,16 @@ struct HistoryDetailView: View {
                     sslInfo: snapshot.sslInfo,
                     sslLoading: false,
                     sslError: snapshot.sslError,
+                    tlsProvenance: snapshot.provenanceBySection[.ssl],
                     responseRows: DomainViewModel.webResponseRows(from: snapshot),
                     headers: snapshot.httpHeaders,
                     headersLoading: false,
                     headersError: snapshot.httpHeadersError,
+                    httpProvenance: snapshot.provenanceBySection[.httpHeaders],
                     redirects: DomainViewModel.redirectRows(from: snapshot),
                     redirectLoading: false,
                     redirectError: snapshot.redirectChainError,
+                    redirectProvenance: snapshot.provenanceBySection[.redirectChain],
                     finalURL: snapshot.redirectChain.last?.url
                 )
                 .padding(.top, appDensity.metrics.sectionSpacing)
@@ -222,6 +248,8 @@ struct HistoryDetailView: View {
                     isCollapsed: .constant(false),
                     rows: DomainViewModel.emailRows(from: snapshot),
                     loading: false,
+                    provenance: snapshot.provenanceBySection[.emailSecurity],
+                    confidence: snapshot.emailSecurityConfidence,
                     error: snapshot.emailSecurityError
                 )
                 .padding(.top, appDensity.metrics.sectionSpacing)
@@ -230,14 +258,18 @@ struct HistoryDetailView: View {
                     reachabilityRows: DomainViewModel.reachabilityRows(from: snapshot),
                     reachabilityLoading: false,
                     reachabilityError: snapshot.reachabilityError,
+                    reachabilityProvenance: snapshot.provenanceBySection[.reachability],
                     locationRows: DomainViewModel.locationRows(from: snapshot),
                     geolocation: snapshot.ipGeolocation,
                     geolocationLoading: false,
                     geolocationError: snapshot.ipGeolocationError,
+                    geolocationProvenance: snapshot.provenanceBySection[.ipGeolocation],
+                    geolocationConfidence: snapshot.geolocationConfidence,
                     standardPortRows: DomainViewModel.portRows(from: snapshot, kind: .standard),
                     customPortRows: DomainViewModel.portRows(from: snapshot, kind: .custom),
                     portScanLoading: false,
                     portScanError: snapshot.portScanError,
+                    portScanProvenance: snapshot.provenanceBySection[.portScan],
                     customPortScanLoading: false,
                     customPortScanError: nil,
                     isCloudflareProxied: snapshot.httpHeaders.contains(where: { $0.name.lowercased() == "cf-ray" }),
@@ -253,26 +285,84 @@ struct HistoryDetailView: View {
         .background(Color.black)
         .navigationTitle(entry.domain)
         .toolbar {
-            Button("Re-run") {
-                viewModel.rerunLookup(from: entry)
+            ToolbarItemGroup(placement: .topBarTrailing) {
+                Button("Note") {
+                    noteDraft = entry.note ?? ""
+                    isEditingNote = true
+                }
+                Button("Re-run") {
+                    showRerunOptions = true
+                }
             }
         }
         .onChange(of: viewModel.rerunNavigationToken) { _, _ in
             dismiss()
         }
+        .confirmationDialog("Re-run lookup", isPresented: $showRerunOptions) {
+            Button("Run with Current Settings") {
+                viewModel.rerunLookup(from: entry, useSnapshotResolver: false)
+            }
+            Button("Run with Snapshot Resolver") {
+                viewModel.rerunLookup(from: entry, useSnapshotResolver: true)
+            }
+            Button("Cancel", role: .cancel) {}
+        } message: {
+            Text(viewModel.resolverMismatchNote(for: entry) ?? "Choose how to reproduce this snapshot.")
+        }
+        .sheet(isPresented: $isEditingNote) {
+            NavigationStack {
+                Form {
+                    Section("Audit Note") {
+                        TextField("Optional note", text: $noteDraft, axis: .vertical)
+                            .lineLimit(3...6)
+                    }
+                }
+                .navigationTitle(entry.domain)
+                .toolbar {
+                    ToolbarItem(placement: .cancellationAction) {
+                        Button("Cancel") {
+                            isEditingNote = false
+                        }
+                    }
+                    ToolbarItem(placement: .confirmationAction) {
+                        Button("Save") {
+                            viewModel.updateHistoryNote(noteDraft, for: entry)
+                            isEditingNote = false
+                        }
+                    }
+                }
+            }
+        }
         .preferredColorScheme(.dark)
     }
 
     private var snapshotBanner: some View {
-        HStack(spacing: 8) {
-            Image(systemName: "archivebox")
-                .font(.caption)
-            Text("Snapshot from \(dateFormatter.string(from: entry.timestamp))")
-                .font(appDensity.font(.caption))
-            Spacer()
-            Text("Live re-run available")
-                .font(appDensity.font(.caption2))
-                .foregroundStyle(.secondary)
+        VStack(alignment: .leading, spacing: 8) {
+            HStack(spacing: 8) {
+                Image(systemName: "archivebox")
+                    .font(.caption)
+                Text("Snapshot from \(dateFormatter.string(from: entry.timestamp))")
+                    .font(appDensity.font(.caption))
+                Spacer()
+                Text("Live re-run available")
+                    .font(appDensity.font(.caption2))
+                    .foregroundStyle(.secondary)
+            }
+            if let mismatchNote = viewModel.resolverMismatchNote(for: entry) {
+                Text(mismatchNote)
+                    .font(appDensity.font(.caption2))
+                    .foregroundStyle(.orange)
+            }
+            if entry.isPartialSnapshot {
+                Text("Partial snapshot: \(entry.validationIssues.joined(separator: " | "))")
+                    .font(appDensity.font(.caption2))
+                    .foregroundStyle(.yellow)
+            }
+            if let note = entry.note, !note.isEmpty {
+                Text(note)
+                    .font(appDensity.font(.caption2))
+                    .foregroundStyle(.secondary)
+            }
         }
         .foregroundStyle(.secondary)
         .padding(8)
diff --git a/DomainDig/LookupRuntime.swift b/DomainDig/LookupRuntime.swift
index 08ea122..5db3134 100644
--- a/DomainDig/LookupRuntime.swift
+++ b/DomainDig/LookupRuntime.swift
@@ -79,15 +79,9 @@ actor LookupRuntime {
     }
 
     func availability(domain: String) async -> CachedLookupResult<DomainAvailabilityResult> {
-        await execute(
-            key: .domain(domain, .availability),
-            extract: { payload in
-                guard case let .availability(result) = payload else { return nil }
-                return result
-            },
-            operation: {
-                .availability(await DomainAvailabilityService.check(domain: domain))
-            }
+        CachedLookupResult(
+            value: await DomainAvailabilityService.check(domain: domain),
+            source: .live
         )
     }
 
diff --git a/DomainDig/Models.swift b/DomainDig/Models.swift
index 85136d2..60e5852 100644
--- a/DomainDig/Models.swift
+++ b/DomainDig/Models.swift
@@ -6,6 +6,59 @@ enum ServiceResult<Value> {
     case error(String)
 }
 
+enum ConfidenceLevel: String, Codable {
+    case high
+    case medium
+    case low
+
+    var title: String {
+        rawValue.capitalized
+    }
+}
+
+enum InspectionErrorKind: String, Codable {
+    case network
+    case timeout
+    case rateLimited
+    case parsing
+    case unsupported
+    case unavailable
+    case unknown
+
+    var title: String {
+        switch self {
+        case .network:
+            return "Network"
+        case .timeout:
+            return "Timeout"
+        case .rateLimited:
+            return "Rate limited"
+        case .parsing:
+            return "Parsing"
+        case .unsupported:
+            return "Unsupported"
+        case .unavailable:
+            return "Unavailable"
+        case .unknown:
+            return "Unknown"
+        }
+    }
+}
+
+struct InspectionFailure: Codable, Equatable {
+    let kind: InspectionErrorKind
+    let message: String
+    let details: String?
+}
+
+struct SectionProvenance: Codable, Equatable {
+    let source: String
+    let collectedAt: Date
+    let provider: String?
+    let resolver: String?
+    let resultSource: LookupResultSource
+}
+
 enum LookupResultSource: String, Codable {
     case live
     case cached
@@ -129,19 +182,28 @@ struct DomainChangeSummary: Codable, Equatable {
     let message: String
     let severity: ChangeSeverity
     let generatedAt: Date
+    let observedFacts: [String]
+    let inferredConclusions: [String]
+    let contextNote: String?
 
     init(
         hasChanges: Bool,
         changedSections: [String],
         message: String,
         severity: ChangeSeverity,
-        generatedAt: Date
+        generatedAt: Date,
+        observedFacts: [String] = [],
+        inferredConclusions: [String] = [],
+        contextNote: String? = nil
     ) {
         self.hasChanges = hasChanges
         self.changedSections = changedSections
         self.message = message
         self.severity = severity
         self.generatedAt = generatedAt
+        self.observedFacts = observedFacts
+        self.inferredConclusions = inferredConclusions
+        self.contextNote = contextNote
     }
 
     init(from decoder: Decoder) throws {
@@ -152,6 +214,9 @@ struct DomainChangeSummary: Codable, Equatable {
         severity = try container.decodeIfPresent(ChangeSeverity.self, forKey: .severity) ?? (hasChanges ? .medium : .low)
         message = try container.decodeIfPresent(String.self, forKey: .message)
             ?? (changedSections.isEmpty ? "No meaningful changes" : changedSections.joined(separator: " • "))
+        observedFacts = try container.decodeIfPresent([String].self, forKey: .observedFacts) ?? []
+        inferredConclusions = try container.decodeIfPresent([String].self, forKey: .inferredConclusions) ?? []
+        contextNote = try container.decodeIfPresent(String.self, forKey: .contextNote)
     }
 }
 
@@ -709,6 +774,7 @@ struct HistoryEntry: Identifiable, Codable {
     let domain: String
     let timestamp: Date
     var trackedDomainID: UUID?
+    var note: String?
     let dnsSections: [DNSSection]
     let sslInfo: SSLCertificateInfo?
     let httpHeaders: [HTTPHeader]
@@ -724,6 +790,18 @@ struct HistoryEntry: Identifiable, Codable {
     var hstsPreloaded: Bool?
     var availabilityResult: DomainAvailabilityResult?
     var suggestions: [DomainSuggestionResult]
+    var appVersion: String
+    var resultSource: LookupResultSource
+    var dataSources: [String]
+    var provenanceBySection: [LookupSectionKind: SectionProvenance]
+    var availabilityConfidence: ConfidenceLevel?
+    var ownershipConfidence: ConfidenceLevel?
+    var subdomainConfidence: ConfidenceLevel?
+    var emailSecurityConfidence: ConfidenceLevel?
+    var geolocationConfidence: ConfidenceLevel?
+    var errorDetails: [LookupSectionKind: InspectionFailure]
+    var isPartialSnapshot: Bool
+    var validationIssues: [String]
     var resolverDisplayName: String
     var resolverURLString: String
     var totalLookupDurationMs: Int?
@@ -744,14 +822,21 @@ struct HistoryEntry: Identifiable, Codable {
     var subdomainsError: String?
     var portScanError: String?
 
-    init(domain: String, timestamp: Date, trackedDomainID: UUID? = nil, dnsSections: [DNSSection],
+    init(domain: String, timestamp: Date, trackedDomainID: UUID? = nil, note: String? = nil, dnsSections: [DNSSection],
          sslInfo: SSLCertificateInfo?, httpHeaders: [HTTPHeader],
          reachabilityResults: [PortReachability], ipGeolocation: IPGeolocation?,
          emailSecurity: EmailSecurityResult? = nil, mtaSts: MTASTSResult? = nil, ownership: DomainOwnership? = nil,
          ptrRecord: String? = nil, redirectChain: [RedirectHop] = [], subdomains: [DiscoveredSubdomain] = [],
          portScanResults: [PortScanResult] = [],
          hstsPreloaded: Bool? = nil, availabilityResult: DomainAvailabilityResult? = nil,
-         suggestions: [DomainSuggestionResult] = [], resolverDisplayName: String, resolverURLString: String,
+         suggestions: [DomainSuggestionResult] = [], appVersion: String = "2.7.0",
+         resultSource: LookupResultSource = .snapshot, dataSources: [String] = [],
+         provenanceBySection: [LookupSectionKind: SectionProvenance] = [:],
+         availabilityConfidence: ConfidenceLevel? = nil, ownershipConfidence: ConfidenceLevel? = nil,
+         subdomainConfidence: ConfidenceLevel? = nil, emailSecurityConfidence: ConfidenceLevel? = nil,
+         geolocationConfidence: ConfidenceLevel? = nil,
+         errorDetails: [LookupSectionKind: InspectionFailure] = [:], isPartialSnapshot: Bool = false,
+         validationIssues: [String] = [], resolverDisplayName: String, resolverURLString: String,
          totalLookupDurationMs: Int? = nil, primaryIP: String? = nil, finalRedirectURL: String? = nil,
          tlsStatusSummary: String? = nil, emailSecuritySummary: String? = nil, httpGradeSummary: String? = nil,
          changeSummary: DomainChangeSummary? = nil, sslError: String? = nil, httpHeadersError: String? = nil,
@@ -761,6 +846,7 @@ struct HistoryEntry: Identifiable, Codable {
         self.domain = domain
         self.timestamp = timestamp
         self.trackedDomainID = trackedDomainID
+        self.note = note
         self.dnsSections = dnsSections
         self.sslInfo = sslInfo
         self.httpHeaders = httpHeaders
@@ -776,6 +862,18 @@ struct HistoryEntry: Identifiable, Codable {
         self.hstsPreloaded = hstsPreloaded
         self.availabilityResult = availabilityResult
         self.suggestions = suggestions
+        self.appVersion = appVersion
+        self.resultSource = resultSource
+        self.dataSources = dataSources
+        self.provenanceBySection = provenanceBySection
+        self.availabilityConfidence = availabilityConfidence
+        self.ownershipConfidence = ownershipConfidence
+        self.subdomainConfidence = subdomainConfidence
+        self.emailSecurityConfidence = emailSecurityConfidence
+        self.geolocationConfidence = geolocationConfidence
+        self.errorDetails = errorDetails
+        self.isPartialSnapshot = isPartialSnapshot
+        self.validationIssues = validationIssues
         self.resolverDisplayName = resolverDisplayName
         self.resolverURLString = resolverURLString
         self.totalLookupDurationMs = totalLookupDurationMs
@@ -800,13 +898,14 @@ struct HistoryEntry: Identifiable, Codable {
     init(from decoder: Decoder) throws {
         let container = try decoder.container(keyedBy: CodingKeys.self)
         id = try container.decodeIfPresent(UUID.self, forKey: .id) ?? UUID()
-        domain = try container.decode(String.self, forKey: .domain)
-        timestamp = try container.decode(Date.self, forKey: .timestamp)
+        domain = try container.decodeIfPresent(String.self, forKey: .domain) ?? "unknown-domain"
+        timestamp = try container.decodeIfPresent(Date.self, forKey: .timestamp) ?? .distantPast
         trackedDomainID = try container.decodeIfPresent(UUID.self, forKey: .trackedDomainID)
-        dnsSections = try container.decode([DNSSection].self, forKey: .dnsSections)
+        note = try container.decodeIfPresent(String.self, forKey: .note)
+        dnsSections = try container.decodeIfPresent([DNSSection].self, forKey: .dnsSections) ?? []
         sslInfo = try container.decodeIfPresent(SSLCertificateInfo.self, forKey: .sslInfo)
-        httpHeaders = try container.decode([HTTPHeader].self, forKey: .httpHeaders)
-        reachabilityResults = try container.decode([PortReachability].self, forKey: .reachabilityResults)
+        httpHeaders = try container.decodeIfPresent([HTTPHeader].self, forKey: .httpHeaders) ?? []
+        reachabilityResults = try container.decodeIfPresent([PortReachability].self, forKey: .reachabilityResults) ?? []
         ipGeolocation = try container.decodeIfPresent(IPGeolocation.self, forKey: .ipGeolocation)
         emailSecurity = try container.decodeIfPresent(EmailSecurityResult.self, forKey: .emailSecurity)
         mtaSts = try container.decodeIfPresent(MTASTSResult.self, forKey: .mtaSts) ?? emailSecurity?.mtaSts
@@ -818,6 +917,18 @@ struct HistoryEntry: Identifiable, Codable {
         hstsPreloaded = try container.decodeIfPresent(Bool.self, forKey: .hstsPreloaded)
         availabilityResult = try container.decodeIfPresent(DomainAvailabilityResult.self, forKey: .availabilityResult)
         suggestions = try container.decodeIfPresent([DomainSuggestionResult].self, forKey: .suggestions) ?? []
+        appVersion = try container.decodeIfPresent(String.self, forKey: .appVersion) ?? "2.6.0"
+        resultSource = try container.decodeIfPresent(LookupResultSource.self, forKey: .resultSource) ?? .snapshot
+        dataSources = try container.decodeIfPresent([String].self, forKey: .dataSources) ?? []
+        provenanceBySection = try container.decodeIfPresent([LookupSectionKind: SectionProvenance].self, forKey: .provenanceBySection) ?? [:]
+        availabilityConfidence = try container.decodeIfPresent(ConfidenceLevel.self, forKey: .availabilityConfidence)
+        ownershipConfidence = try container.decodeIfPresent(ConfidenceLevel.self, forKey: .ownershipConfidence)
+        subdomainConfidence = try container.decodeIfPresent(ConfidenceLevel.self, forKey: .subdomainConfidence)
+        emailSecurityConfidence = try container.decodeIfPresent(ConfidenceLevel.self, forKey: .emailSecurityConfidence)
+        geolocationConfidence = try container.decodeIfPresent(ConfidenceLevel.self, forKey: .geolocationConfidence)
+        errorDetails = try container.decodeIfPresent([LookupSectionKind: InspectionFailure].self, forKey: .errorDetails) ?? [:]
+        validationIssues = try container.decodeIfPresent([String].self, forKey: .validationIssues) ?? HistoryEntry.defaultValidationIssues(domain: domain, timestamp: timestamp)
+        isPartialSnapshot = try container.decodeIfPresent(Bool.self, forKey: .isPartialSnapshot) ?? !validationIssues.isEmpty
         resolverDisplayName = try container.decodeIfPresent(String.self, forKey: .resolverDisplayName) ?? "Cloudflare"
         resolverURLString = try container.decodeIfPresent(String.self, forKey: .resolverURLString) ?? DNSResolverOption.defaultURLString
         totalLookupDurationMs = try container.decodeIfPresent(Int.self, forKey: .totalLookupDurationMs)
@@ -838,6 +949,17 @@ struct HistoryEntry: Identifiable, Codable {
         subdomainsError = try container.decodeIfPresent(String.self, forKey: .subdomainsError)
         portScanError = try container.decodeIfPresent(String.self, forKey: .portScanError)
     }
+
+    private static func defaultValidationIssues(domain: String, timestamp: Date) -> [String] {
+        var issues: [String] = []
+        if domain == "unknown-domain" {
+            issues.append("Missing domain in stored snapshot")
+        }
+        if timestamp == .distantPast {
+            issues.append("Missing collection timestamp in stored snapshot")
+        }
+        return issues
+    }
 }
 
 // MARK: - Cloudflare DNS-over-HTTPS Response
diff --git a/DomainDig/WatchlistView.swift b/DomainDig/WatchlistView.swift
index 0874956..d58061f 100644
--- a/DomainDig/WatchlistView.swift
+++ b/DomainDig/WatchlistView.swift
@@ -334,6 +334,7 @@ struct TrackedDomainDetailView: View {
 
     @State private var noteDraft = ""
     @State private var isEditingNote = false
+    @State private var showRerunOptions = false
 
     private var liveTrackedDomain: TrackedDomain {
         viewModel.trackedDomains.first(where: { $0.id == trackedDomain.id }) ?? trackedDomain
@@ -365,7 +366,7 @@ struct TrackedDomainDetailView: View {
                 }
 
                 Button {
-                    viewModel.rerunInspection(for: liveTrackedDomain)
+                    showRerunOptions = true
                 } label: {
                     Label("Re-run Inspection", systemImage: "magnifyingglass")
                 }
@@ -394,7 +395,14 @@ struct TrackedDomainDetailView: View {
 
             if !latestDiffSections.isEmpty {
                 Section("Latest Diff") {
-                    DomainDiffView(title: "Latest Snapshot vs Previous", sections: latestDiffSections, showsUnchanged: false)
+                    DomainDiffView(
+                        title: "Latest Snapshot vs Previous",
+                        sections: latestDiffSections,
+                        contextNote: latestSnapshots.count >= 2
+                            ? DomainDiffService.comparisonContextNote(from: latestSnapshots[1].snapshot, to: latestSnapshots[0].snapshot)
+                            : nil,
+                        showsUnchanged: false
+                    )
                 }
                 .listRowBackground(Color.clear)
             }
@@ -430,6 +438,19 @@ struct TrackedDomainDetailView: View {
         .onChange(of: viewModel.rerunNavigationToken) { _, _ in
             dismiss()
         }
+        .confirmationDialog("Re-run inspection", isPresented: $showRerunOptions) {
+            Button("Run with Current Settings") {
+                viewModel.rerunInspection(for: liveTrackedDomain, useSnapshotResolver: false)
+            }
+            if latestSnapshots.first != nil {
+                Button("Run with Snapshot Resolver") {
+                    viewModel.rerunInspection(for: liveTrackedDomain, useSnapshotResolver: true)
+                }
+            }
+            Button("Cancel", role: .cancel) {}
+        } message: {
+            Text(viewModel.resolverMismatchNote(for: liveTrackedDomain) ?? "Choose how to reproduce the most recent snapshot.")
+        }
         .sheet(isPresented: $isEditingNote) {
             NavigationStack {
                 Form {
diff --git a/DomainInspectionService.swift b/DomainInspectionService.swift
index cb9350b..b906b83 100644
--- a/DomainInspectionService.swift
+++ b/DomainInspectionService.swift
@@ -20,6 +20,9 @@ struct DomainInspectionService {
         let resolverURLString = DNSLookupService.currentResolverURLString()
         var cachedSections = Set<LookupSectionKind>()
         var sectionSources: [LookupResultSource] = []
+        var provenanceBySection: [LookupSectionKind: SectionProvenance] = [:]
+        var dataSources = Set<String>()
+        var errorDetails: [LookupSectionKind: InspectionFailure] = [:]
 
         async let dnsFetch = runtime.dns(domain: normalizedDomain)
         async let availabilityFetch = runtime.availability(domain: normalizedDomain)
@@ -34,41 +37,129 @@ struct DomainInspectionService {
 
         let resolvedDNS = await dnsFetch
         let dnsResult = normalizeErrors(in: resolvedDNS.value)
-        track(.dns, source: resolvedDNS.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+        track(
+            .dns,
+            source: resolvedDNS.source,
+            provenance: provenance(for: .dns, source: resolvedDNS.source, collectedAt: Date(), resolverDisplayName: resolverDisplayName),
+            cachedSections: &cachedSections,
+            sectionSources: &sectionSources,
+            provenanceBySection: &provenanceBySection,
+            dataSources: &dataSources
+        )
+        captureFailure(for: .dns, result: dnsResult, into: &errorDetails)
 
         let availability = await availabilityFetch
-        track(.availability, source: availability.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+        track(
+            .availability,
+            source: availability.source,
+            provenance: provenance(for: .availability, source: availability.source, collectedAt: Date(), resolverDisplayName: resolverDisplayName),
+            cachedSections: &cachedSections,
+            sectionSources: &sectionSources,
+            provenanceBySection: &provenanceBySection,
+            dataSources: &dataSources
+        )
 
         let resolvedSSL = await sslFetch
         let sslResult = normalizeErrors(in: resolvedSSL.value)
-        track(.ssl, source: resolvedSSL.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+        track(
+            .ssl,
+            source: resolvedSSL.source,
+            provenance: provenance(for: .ssl, source: resolvedSSL.source, collectedAt: Date(), resolverDisplayName: resolverDisplayName),
+            cachedSections: &cachedSections,
+            sectionSources: &sectionSources,
+            provenanceBySection: &provenanceBySection,
+            dataSources: &dataSources
+        )
+        captureFailure(for: .ssl, result: sslResult, into: &errorDetails)
 
         let hsts = await hstsFetch
-        track(.hsts, source: hsts.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+        track(
+            .hsts,
+            source: hsts.source,
+            provenance: provenance(for: .hsts, source: hsts.source, collectedAt: Date(), resolverDisplayName: resolverDisplayName),
+            cachedSections: &cachedSections,
+            sectionSources: &sectionSources,
+            provenanceBySection: &provenanceBySection,
+            dataSources: &dataSources
+        )
 
         let http = await httpFetch
         let httpResult = normalizeErrors(in: http.value)
-        track(.httpHeaders, source: http.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+        track(
+            .httpHeaders,
+            source: http.source,
+            provenance: provenance(for: .httpHeaders, source: http.source, collectedAt: Date(), resolverDisplayName: resolverDisplayName),
+            cachedSections: &cachedSections,
+            sectionSources: &sectionSources,
+            provenanceBySection: &provenanceBySection,
+            dataSources: &dataSources
+        )
+        captureFailure(for: .httpHeaders, result: httpResult, into: &errorDetails)
 
         let reachability = await reachabilityFetch
         let reachabilityResult = normalizeErrors(in: reachability.value)
-        track(.reachability, source: reachability.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+        track(
+            .reachability,
+            source: reachability.source,
+            provenance: provenance(for: .reachability, source: reachability.source, collectedAt: Date(), resolverDisplayName: resolverDisplayName),
+            cachedSections: &cachedSections,
+            sectionSources: &sectionSources,
+            provenanceBySection: &provenanceBySection,
+            dataSources: &dataSources
+        )
+        captureFailure(for: .reachability, result: reachabilityResult, into: &errorDetails)
 
         let resolvedOwnership = await ownershipFetch
         let ownershipResult = normalizeErrors(in: resolvedOwnership.value)
-        track(.ownership, source: resolvedOwnership.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+        track(
+            .ownership,
+            source: resolvedOwnership.source,
+            provenance: provenance(for: .ownership, source: resolvedOwnership.source, collectedAt: Date(), resolverDisplayName: resolverDisplayName),
+            cachedSections: &cachedSections,
+            sectionSources: &sectionSources,
+            provenanceBySection: &provenanceBySection,
+            dataSources: &dataSources
+        )
+        captureFailure(for: .ownership, result: ownershipResult, into: &errorDetails)
 
         let redirects = await redirectFetch
         let redirectResult = normalizeErrors(in: redirects.value)
-        track(.redirectChain, source: redirects.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+        track(
+            .redirectChain,
+            source: redirects.source,
+            provenance: provenance(for: .redirectChain, source: redirects.source, collectedAt: Date(), resolverDisplayName: resolverDisplayName),
+            cachedSections: &cachedSections,
+            sectionSources: &sectionSources,
+            provenanceBySection: &provenanceBySection,
+            dataSources: &dataSources
+        )
+        captureFailure(for: .redirectChain, result: redirectResult, into: &errorDetails)
 
         let resolvedSubdomains = await subdomainFetch
         let subdomainResult = normalizeErrors(in: resolvedSubdomains.value)
-        track(.subdomains, source: resolvedSubdomains.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+        track(
+            .subdomains,
+            source: resolvedSubdomains.source,
+            provenance: provenance(for: .subdomains, source: resolvedSubdomains.source, collectedAt: Date(), resolverDisplayName: resolverDisplayName),
+            cachedSections: &cachedSections,
+            sectionSources: &sectionSources,
+            provenanceBySection: &provenanceBySection,
+            dataSources: &dataSources
+        )
+        captureFailure(for: .subdomains, result: subdomainResult, into: &errorDetails)
 
         let ports = await portScanFetch
         let portScanResult = normalizeErrors(in: ports.value)
-        track(.portScan, source: ports.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+        track(
+            .portScan,
+            source: ports.source,
+            provenance: provenance(for: .portScan, source: ports.source, collectedAt: Date(), resolverDisplayName: resolverDisplayName),
+            cachedSections: &cachedSections,
+            sectionSources: &sectionSources,
+            provenanceBySection: &provenanceBySection,
+            dataSources: &dataSources
+        )
+        captureFailure(for: .portScan, result: portScanResult, into: &errorDetails)
 
         let dnsSections = mapServiceResult(dnsResult, emptyValue: [])
         let sslInfo = mapOptionalValueServiceResult(sslResult)
@@ -92,12 +183,30 @@ struct DomainInspectionService {
         } else {
             emailOutcome = await runtime.email(domain: normalizedDomain, txtRecords: txtRecords)
         }
-        track(.emailSecurity, source: emailOutcome.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+        let normalizedEmailResult = normalizeErrors(in: emailOutcome.value)
+        track(
+            .emailSecurity,
+            source: emailOutcome.source,
+            provenance: provenance(for: .emailSecurity, source: emailOutcome.source, collectedAt: Date(), resolverDisplayName: resolverDisplayName),
+            cachedSections: &cachedSections,
+            sectionSources: &sectionSources,
+            provenanceBySection: &provenanceBySection,
+            dataSources: &dataSources
+        )
+        captureFailure(for: .emailSecurity, result: normalizedEmailResult, into: &errorDetails)
 
         let suggestionsOutcome: CachedLookupResult<[DomainSuggestionResult]>
         if availability.value.status == .registered {
             suggestionsOutcome = await runtime.suggestions(domain: normalizedDomain)
-            track(.suggestions, source: suggestionsOutcome.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+            track(
+                .suggestions,
+                source: suggestionsOutcome.source,
+                provenance: provenance(for: .suggestions, source: suggestionsOutcome.source, collectedAt: Date(), resolverDisplayName: resolverDisplayName),
+                cachedSections: &cachedSections,
+                sectionSources: &sectionSources,
+                provenanceBySection: &provenanceBySection,
+                dataSources: &dataSources
+            )
         } else {
             suggestionsOutcome = CachedLookupResult(value: [], source: .live)
         }
@@ -122,27 +231,65 @@ struct DomainInspectionService {
             }
 
             if let ptrOutcome {
-                track(.ptr, source: ptrOutcome.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+                let normalizedPTRResult = normalizeErrors(in: ptrOutcome.value)
+                track(
+                    .ptr,
+                    source: ptrOutcome.source,
+                    provenance: provenance(for: .ptr, source: ptrOutcome.source, collectedAt: Date(), resolverDisplayName: resolverDisplayName),
+                    cachedSections: &cachedSections,
+                    sectionSources: &sectionSources,
+                    provenanceBySection: &provenanceBySection,
+                    dataSources: &dataSources
+                )
+                captureFailure(for: .ptr, result: normalizedPTRResult, into: &errorDetails)
             }
             if let geoOutcome {
-                track(.ipGeolocation, source: geoOutcome.source, cachedSections: &cachedSections, sectionSources: &sectionSources)
+                let normalizedGeoResult = normalizeErrors(in: geoOutcome.value)
+                track(
+                    .ipGeolocation,
+                    source: geoOutcome.source,
+                    provenance: provenance(for: .ipGeolocation, source: geoOutcome.source, collectedAt: Date(), resolverDisplayName: resolverDisplayName),
+                    cachedSections: &cachedSections,
+                    sectionSources: &sectionSources,
+                    provenanceBySection: &provenanceBySection,
+                    dataSources: &dataSources
+                )
+                captureFailure(for: .ipGeolocation, result: normalizedGeoResult, into: &errorDetails)
             }
         } else {
             ptrOutcome = nil
             geoOutcome = nil
         }
 
-        let emailSecurity = mapOptionalValueServiceResult(normalizeErrors(in: emailOutcome.value))
+        let emailSecurity = mapOptionalValueServiceResult(normalizedEmailResult)
         let ptrRecord = mapOptionalServiceResult(ptrOutcome.map { normalizeErrors(in: $0.value) }, missingMessage: "No A record available")
         let geolocation = mapOptionalServiceResult(geoOutcome.map { normalizeErrors(in: $0.value) }, missingMessage: "No A record available")
+        let availabilityConfidence = confidenceForAvailability(result: availability.value, provenance: provenanceBySection[.availability])
+        let ownershipConfidence = confidenceForOwnership(result: ownership.value, error: ownership.message)
+        let subdomainConfidence = confidenceForSubdomains(results: subdomains.value, error: subdomains.message)
+        let emailConfidence = confidenceForEmail(result: emailSecurity.value, error: emailSecurity.message)
+        let geolocationConfidence = confidenceForGeolocation(result: geolocation.value, error: geolocation.message)
+        let validationIssues = validationIssues(for: normalizedDomain, snapshotTimestamp: startedAt, availability: availability.value, dnsSections: dnsSections.value, provenanceBySection: provenanceBySection)
 
         return LookupSnapshot(
             historyEntryID: nil,
             domain: availability.value.domain,
             timestamp: Date(),
             trackedDomainID: previousSnapshot?.trackedDomainID,
+            note: previousSnapshot?.note,
+            appVersion: AppVersion.current,
             resolverDisplayName: resolverDisplayName,
             resolverURLString: resolverURLString,
+            dataSources: Array(dataSources).sorted(),
+            provenanceBySection: provenanceBySection,
+            availabilityConfidence: availabilityConfidence,
+            ownershipConfidence: ownershipConfidence,
+            subdomainConfidence: subdomainConfidence,
+            emailSecurityConfidence: emailConfidence,
+            geolocationConfidence: geolocationConfidence,
+            errorDetails: errorDetails,
+            isPartialSnapshot: !validationIssues.isEmpty,
+            validationIssues: validationIssues,
             totalLookupDurationMs: Int(Date().timeIntervalSince(startedAt) * 1000),
             dnsSections: dnsSections.value,
             dnsError: dnsSections.message,
@@ -193,15 +340,64 @@ struct DomainInspectionService {
     private func track(
         _ section: LookupSectionKind,
         source: LookupResultSource,
+        provenance: SectionProvenance,
         cachedSections: inout Set<LookupSectionKind>,
-        sectionSources: inout [LookupResultSource]
+        sectionSources: inout [LookupResultSource],
+        provenanceBySection: inout [LookupSectionKind: SectionProvenance],
+        dataSources: inout Set<String>
     ) {
         sectionSources.append(source)
+        provenanceBySection[section] = provenance
+        dataSources.insert(provenance.provider ?? provenance.source)
         if source != .live {
             cachedSections.insert(section)
         }
     }
 
+    private func provenance(
+        for section: LookupSectionKind,
+        source: LookupResultSource,
+        collectedAt: Date,
+        resolverDisplayName: String
+    ) -> SectionProvenance {
+        switch section {
+        case .dns, .ptr:
+            return SectionProvenance(
+                source: "DNS-over-HTTPS query",
+                collectedAt: collectedAt,
+                provider: "Selected DoH resolver",
+                resolver: resolverDisplayName,
+                resultSource: source
+            )
+        case .availability:
+            return SectionProvenance(
+                source: "RDAP lookup with DNS fallback",
+                collectedAt: collectedAt,
+                provider: "rdap.org / selected resolver",
+                resolver: resolverDisplayName,
+                resultSource: source
+            )
+        case .ssl:
+            return SectionProvenance(source: "Direct TLS handshake", collectedAt: collectedAt, provider: "Target host", resolver: nil, resultSource: source)
+        case .hsts, .httpHeaders, .redirectChain:
+            return SectionProvenance(source: "HTTP request", collectedAt: collectedAt, provider: "Target host", resolver: nil, resultSource: source)
+        case .reachability:
+            return SectionProvenance(source: "TCP reachability probe", collectedAt: collectedAt, provider: "Target host", resolver: nil, resultSource: source)
+        case .ipGeolocation:
+            return SectionProvenance(source: "IP geolocation lookup", collectedAt: collectedAt, provider: "ipapi.co", resolver: nil, resultSource: source)
+        case .emailSecurity:
+            return SectionProvenance(source: "DNS TXT inspection", collectedAt: collectedAt, provider: "Selected DoH resolver", resolver: resolverDisplayName, resultSource: source)
+        case .ownership:
+            return SectionProvenance(source: "RDAP domain lookup", collectedAt: collectedAt, provider: "rdap.org", resolver: nil, resultSource: source)
+        case .subdomains:
+            return SectionProvenance(source: "Certificate transparency search", collectedAt: collectedAt, provider: "crt.sh", resolver: nil, resultSource: source)
+        case .portScan:
+            return SectionProvenance(source: "TCP port scan", collectedAt: collectedAt, provider: "Target host", resolver: nil, resultSource: source)
+        case .suggestions:
+            return SectionProvenance(source: "Availability suggestions", collectedAt: collectedAt, provider: "DomainDig heuristic", resolver: resolverDisplayName, resultSource: source)
+        }
+    }
+
     private func aggregateSource(_ sectionSources: [LookupResultSource]) -> LookupResultSource {
         let normalizedSources = sectionSources.map { source -> LookupResultSource in
             source == .mixed ? .cached : source
@@ -259,44 +455,131 @@ struct DomainInspectionService {
         case let .success(value):
             return .success(value)
         case let .empty(message):
-            return .empty(message)
+            return .empty(classifyFailure(from: message, defaultKind: .unavailable).message)
         case let .error(message):
-            return .error(classifiedMessage(from: message))
+            return .error(classifyFailure(from: message).message)
         }
     }
 
-    private func classifiedMessage(from message: String) -> String {
+    private func classifyFailure(from message: String, defaultKind: InspectionErrorKind = .unknown) -> InspectionFailure {
         let normalizedMessage = message.trimmingCharacters(in: .whitespacesAndNewlines)
         let lowercasedMessage = normalizedMessage.lowercased()
-
-        if lowercasedMessage.hasPrefix("network error:")
-            || lowercasedMessage.hasPrefix("timeout:")
-            || lowercasedMessage.hasPrefix("rate limit:")
-            || lowercasedMessage.hasPrefix("parsing error:") {
-            return normalizedMessage
-        }
-
         if lowercasedMessage.contains("timed out") {
-            return "Timeout: Request timed out"
+            return InspectionFailure(kind: .timeout, message: "Timed out", details: normalizedMessage)
         }
         if lowercasedMessage.contains("429")
             || lowercasedMessage.contains("too many requests")
             || lowercasedMessage.contains("rate limit") {
-            return "Rate limit: Try again shortly"
+            return InspectionFailure(kind: .rateLimited, message: "Rate limited", details: normalizedMessage)
         }
         if lowercasedMessage.contains("cannot parse")
             || lowercasedMessage.contains("decoding")
             || lowercasedMessage.contains("json") {
-            return "Parsing error: Invalid server response"
+            return InspectionFailure(kind: .parsing, message: "Could not parse response", details: normalizedMessage)
         }
         if lowercasedMessage.contains("offline")
             || lowercasedMessage.contains("internet connection")
             || lowercasedMessage.contains("not connected")
             || lowercasedMessage.contains("network connection") {
-            return "Network error: Offline"
+            return InspectionFailure(kind: .network, message: "Network unavailable", details: normalizedMessage)
         }
+        if lowercasedMessage.contains("unsupported") {
+            return InspectionFailure(kind: .unsupported, message: "Unsupported for this target", details: normalizedMessage)
+        }
+        if lowercasedMessage == "unavailable" || lowercasedMessage.contains("no a record available") {
+            return InspectionFailure(kind: .unavailable, message: normalizedMessage, details: nil)
+        }
+        if defaultKind == .unavailable {
+            return InspectionFailure(kind: .unavailable, message: normalizedMessage, details: nil)
+        }
+        return InspectionFailure(kind: .unknown, message: normalizedMessage.isEmpty ? defaultKind.title : normalizedMessage, details: normalizedMessage)
+    }
 
-        return "Network error: \(normalizedMessage)"
+    private func captureFailure<Value>(
+        for section: LookupSectionKind,
+        result: ServiceResult<Value>,
+        into errorDetails: inout [LookupSectionKind: InspectionFailure]
+    ) {
+        switch result {
+        case .success:
+            return
+        case let .empty(message):
+            errorDetails[section] = classifyFailure(from: message, defaultKind: .unavailable)
+        case let .error(message):
+            errorDetails[section] = classifyFailure(from: message)
+        }
+    }
+
+    private func confidenceForAvailability(result: DomainAvailabilityResult, provenance: SectionProvenance?) -> ConfidenceLevel {
+        guard result.status != .unknown else { return .low }
+        if provenance?.provider?.localizedCaseInsensitiveContains("rdap.org") == true, result.status == .registered {
+            return .high
+        }
+        if result.status == .registered {
+            return .medium
+        }
+        return .low
+    }
+
+    private func confidenceForOwnership(result: DomainOwnership?, error: String?) -> ConfidenceLevel {
+        guard let result else { return error == nil ? .low : .low }
+        let hasDirectRegistrationData = result.registrar != nil || result.createdDate != nil || result.expirationDate != nil
+        return hasDirectRegistrationData ? .high : .medium
+    }
+
+    private func confidenceForSubdomains(results: [DiscoveredSubdomain], error: String?) -> ConfidenceLevel {
+        if !results.isEmpty {
+            return .medium
+        }
+        return error == nil ? .low : .low
+    }
+
+    private func confidenceForEmail(result: EmailSecurityResult?, error: String?) -> ConfidenceLevel {
+        guard let result else { return error == nil ? .low : .low }
+        let foundCount = [result.spf.found, result.dmarc.found, result.dkim.found, result.bimi.found, result.mtaSts?.txtFound == true]
+            .filter { $0 }
+            .count
+        if foundCount >= 3 {
+            return .high
+        }
+        if foundCount >= 1 {
+            return .medium
+        }
+        return .low
+    }
+
+    private func confidenceForGeolocation(result: IPGeolocation?, error: String?) -> ConfidenceLevel {
+        guard let result else { return error == nil ? .low : .low }
+        if result.city != nil && result.country_name != nil && result.latitude != nil && result.longitude != nil {
+            return .high
+        }
+        if result.country_name != nil || result.org != nil {
+            return .medium
+        }
+        return .low
+    }
+
+    private func validationIssues(
+        for domain: String,
+        snapshotTimestamp: Date,
+        availability: DomainAvailabilityResult,
+        dnsSections: [DNSSection],
+        provenanceBySection: [LookupSectionKind: SectionProvenance]
+    ) -> [String] {
+        var issues: [String] = []
+        if domain.isEmpty {
+            issues.append("Missing normalized domain")
+        }
+        if availability.domain.isEmpty {
+            issues.append("Missing normalized availability domain")
+        }
+        if dnsSections.isEmpty && provenanceBySection[.dns] == nil {
+            issues.append("Missing DNS provenance")
+        }
+        if snapshotTimestamp > Date().addingTimeInterval(5) {
+            issues.append("Snapshot timestamp is in the future")
+        }
+        return issues
     }
 
     private func mapServiceResult<Value>(_ result: ServiceResult<Value>, emptyValue: Value) -> (value: Value, message: String?) {
diff --git a/DomainReportBuilder.swift b/DomainReportBuilder.swift
index 7c5a7c0..765ad4f 100644
--- a/DomainReportBuilder.swift
+++ b/DomainReportBuilder.swift
@@ -3,8 +3,22 @@ import Foundation
 struct DomainReport: Codable {
     let domain: String
     let timestamp: Date
+    let appVersion: String
+    let resolverDisplayName: String
+    let resolverURLString: String
+    let dataSources: [String]
     let resultSource: LookupResultSource
+    let sectionProvenance: [LookupSectionKind: SectionProvenance]
+    let errorDetails: [LookupSectionKind: InspectionFailure]
+    let isPartialSnapshot: Bool
+    let validationIssues: [String]
+    let auditNote: String?
     let availability: DomainAvailabilityStatus
+    let availabilityConfidence: ConfidenceLevel?
+    let ownershipConfidence: ConfidenceLevel?
+    let subdomainConfidence: ConfidenceLevel?
+    let emailConfidence: ConfidenceLevel?
+    let geolocationConfidence: ConfidenceLevel?
     let ownership: DomainOwnership?
     let dns: DNSResultSummary
     let web: WebResultSummary
@@ -71,8 +85,22 @@ struct DomainReportBuilder {
         return DomainReport(
             domain: snapshot.domain,
             timestamp: snapshot.timestamp,
+            appVersion: snapshot.appVersion,
+            resolverDisplayName: snapshot.resolverDisplayName,
+            resolverURLString: snapshot.resolverURLString,
+            dataSources: snapshot.dataSources,
             resultSource: snapshot.resultSource,
+            sectionProvenance: snapshot.provenanceBySection,
+            errorDetails: snapshot.errorDetails,
+            isPartialSnapshot: snapshot.isPartialSnapshot,
+            validationIssues: snapshot.validationIssues,
+            auditNote: snapshot.note,
             availability: snapshot.availabilityResult?.status ?? .unknown,
+            availabilityConfidence: snapshot.availabilityConfidence,
+            ownershipConfidence: snapshot.ownershipConfidence,
+            subdomainConfidence: snapshot.subdomainConfidence,
+            emailConfidence: snapshot.emailSecurityConfidence,
+            geolocationConfidence: snapshot.geolocationConfidence,
             ownership: snapshot.ownership,
             dns: DNSResultSummary(
                 resolverDisplayName: snapshot.resolverDisplayName,
diff --git a/DomainReportExporter.swift b/DomainReportExporter.swift
index ecfdbd4..0841fcf 100644
--- a/DomainReportExporter.swift
+++ b/DomainReportExporter.swift
@@ -36,10 +36,27 @@ enum DomainReportExporter {
             "DomainDig Report",
             "Domain: \(report.domain)",
             "Timestamp: \(textDateFormatter.string(from: report.timestamp))",
+            "App Version: \(report.appVersion)",
+            "Resolver: \(report.resolverDisplayName)",
+            "Resolver URL: \(report.resolverURLString)",
             "Source: \(report.resultSource.label)",
-            "Availability: \(availabilityLabel(report.availability))"
+            "Availability: \(availabilityLabel(report.availability))",
+            "Availability Confidence: \(report.availabilityConfidence?.title ?? "N/A")"
         ]
 
+        if report.isPartialSnapshot {
+            lines.append("Snapshot Integrity: Partial snapshot")
+        }
+        if let auditNote = report.auditNote, !auditNote.isEmpty {
+            lines.append("Audit Note: \(auditNote)")
+        }
+        if !report.dataSources.isEmpty {
+            lines.append("Data Sources: \(report.dataSources.joined(separator: ", "))")
+        }
+        if !report.validationIssues.isEmpty {
+            lines.append("Validation: \(report.validationIssues.joined(separator: " | "))")
+        }
+
         appendSection("Summary", to: &lines) {
             [
                 "Primary IP: \(report.dns.primaryIP ?? "Unavailable")",
@@ -53,12 +70,16 @@ enum DomainReportExporter {
         appendSection("Ownership", to: &lines) {
             var ownershipLines = [
                 "Registrar: \(report.ownership?.registrar ?? "Unavailable")",
+                "Confidence: \(report.ownershipConfidence?.title ?? "N/A")",
                 "Created: \(ownershipDateLabel(report.ownership?.createdDate))",
                 "Expires: \(ownershipDateLabel(report.ownership?.expirationDate))",
                 "Nameservers: \(joined(report.ownership?.nameservers) ?? "Unavailable")",
                 "Status: \(joined(report.ownership?.status) ?? "Unavailable")",
                 "Abuse Contact: \(report.ownership?.abuseEmail ?? "Unavailable")"
             ]
+            if let provenance = report.sectionProvenance[.ownership] {
+                ownershipLines.append("Provenance: \(provenanceLabel(provenance))")
+            }
             if let error = report.dns.error, report.ownership == nil {
                 ownershipLines.append("Error: \(error)")
             } else if let error = report.changeSummary?.message, report.ownership == nil, report.ownership == nil {
@@ -69,13 +90,14 @@ enum DomainReportExporter {
 
         appendSection("DNS", to: &lines) {
             var dnsLines = [
-                "Resolver: \(report.dns.resolverDisplayName)",
-                "Resolver URL: \(report.dns.resolverURLString)",
                 "Lookup Duration: \(durationLabel(report.dns.lookupDurationMs))",
                 "Primary IP: \(report.dns.primaryIP ?? "Unavailable")",
                 "PTR: \(report.dns.ptrRecord ?? report.dns.ptrError ?? "Unavailable")",
                 "DNSSEC: \(dnssecLabel(report.dns.dnssecSigned))"
             ]
+            if let provenance = report.sectionProvenance[.dns] {
+                dnsLines.append("Provenance: \(provenanceLabel(provenance))")
+            }
             if let error = report.dns.error {
                 dnsLines.append("Error: \(error)")
             }
@@ -104,6 +126,15 @@ enum DomainReportExporter {
                 "HSTS Preloaded: \(booleanLabel(report.web.hstsPreloaded))",
                 "Header Count: \(report.web.headerCount)"
             ]
+            if let provenance = report.sectionProvenance[.ssl] {
+                webLines.append("TLS Provenance: \(provenanceLabel(provenance))")
+            }
+            if let provenance = report.sectionProvenance[.httpHeaders] {
+                webLines.append("HTTP Provenance: \(provenanceLabel(provenance))")
+            }
+            if let provenance = report.sectionProvenance[.redirectChain] {
+                webLines.append("Redirect Provenance: \(provenanceLabel(provenance))")
+            }
             if let tlsError = report.web.tlsError {
                 webLines.append("TLS Error: \(tlsError)")
             }
@@ -130,6 +161,10 @@ enum DomainReportExporter {
 
         appendSection("Email", to: &lines) {
             var emailLines = [report.email.summary]
+            emailLines.append("Confidence: \(report.emailConfidence?.title ?? "N/A")")
+            if let provenance = report.sectionProvenance[.emailSecurity] {
+                emailLines.append("Provenance: \(provenanceLabel(provenance))")
+            }
             if let records = report.email.records {
                 emailLines.append("SPF: \(recordLabel(records.spf))")
                 emailLines.append("DMARC: \(recordLabel(records.dmarc))")
@@ -147,8 +182,12 @@ enum DomainReportExporter {
             var networkLines = [
                 "Reachability: \(report.network.reachabilitySummary)",
                 "Geolocation: \(report.network.geolocationSummary)",
+                "Geolocation Confidence: \(report.geolocationConfidence?.title ?? "N/A")",
                 "Open Ports: \(report.network.openPorts.map(String.init).joined(separator: ", ").nilIfEmpty ?? "None")"
             ]
+            if let provenance = report.sectionProvenance[.ipGeolocation] {
+                networkLines.append("Geolocation Provenance: \(provenanceLabel(provenance))")
+            }
             if let error = report.network.reachabilityError {
                 networkLines.append("Reachability Error: \(error)")
             }
@@ -170,10 +209,16 @@ enum DomainReportExporter {
         }
 
         appendSection("Subdomains", to: &lines) {
+            var values = ["Confidence: \(report.subdomainConfidence?.title ?? "N/A")"]
+            if let provenance = report.sectionProvenance[.subdomains] {
+                values.append("Provenance: \(provenanceLabel(provenance))")
+            }
             if report.subdomains.isEmpty {
-                return ["None"]
+                values.append("None")
+                return values
             }
-            return report.subdomains.map { "- \($0)" }
+            values.append(contentsOf: report.subdomains.map { "- \($0)" })
+            return values
         }
 
         appendSection("Changes", to: &lines) {
@@ -181,12 +226,19 @@ enum DomainReportExporter {
                 return ["No comparison available"]
             }
 
-            return [
+            var values = [
                 "Has Changes: \(changeSummary.hasChanges ? "Yes" : "No")",
                 "Severity: \(changeSummary.severity.title)",
-                "Summary: \(changeSummary.message)",
+                "Inferred Summary: \(changeSummary.message)",
                 "Changed Sections: \(changeSummary.changedSections.isEmpty ? "None" : changeSummary.changedSections.joined(separator: ", "))"
             ]
+            if !changeSummary.observedFacts.isEmpty {
+                values.append("Observed: \(changeSummary.observedFacts.joined(separator: " | "))")
+            }
+            if let contextNote = changeSummary.contextNote {
+                values.append("Context: \(contextNote)")
+            }
+            return values
         }
 
         return lines.joined(separator: "\n")
@@ -213,9 +265,13 @@ enum DomainReportExporter {
         let headers = [
             "domain",
             "timestamp",
+            "app_version",
             "result_source",
+            "resolver",
             "availability",
+            "availability_confidence",
             "registrar",
+            "ownership_confidence",
             "ownership_expires",
             "nameservers",
             "primary_ip",
@@ -228,11 +284,17 @@ enum DomainReportExporter {
             "http_security_grade",
             "final_url",
             "email_summary",
+            "email_confidence",
             "subdomain_count",
+            "subdomain_confidence",
             "subdomains",
             "open_ports",
             "reachability_summary",
             "geolocation_summary",
+            "geolocation_confidence",
+            "data_sources",
+            "audit_note",
+            "partial_snapshot",
             "change_summary"
         ]
 
@@ -249,9 +311,13 @@ enum DomainReportExporter {
             return [
                 report.domain,
                 csvDateFormatter.string(from: report.timestamp),
+                report.appVersion,
                 report.resultSource.rawValue,
+                report.resolverDisplayName,
                 availabilityLabel(report.availability),
+                report.availabilityConfidence?.rawValue ?? "",
                 report.ownership?.registrar ?? "",
+                report.ownershipConfidence?.rawValue ?? "",
                 expirationDate,
                 nameservers,
                 report.dns.primaryIP ?? "",
@@ -264,11 +330,17 @@ enum DomainReportExporter {
                 report.web.securityGrade ?? "",
                 report.web.finalURL ?? "",
                 report.email.summary,
+                report.emailConfidence?.rawValue ?? "",
                 subdomainCount,
+                report.subdomainConfidence?.rawValue ?? "",
                 subdomains,
                 openPorts,
                 report.network.reachabilitySummary,
                 report.network.geolocationSummary,
+                report.geolocationConfidence?.rawValue ?? "",
+                report.dataSources.joined(separator: " | "),
+                report.auditNote ?? "",
+                report.isPartialSnapshot ? "true" : "false",
                 report.changeSummary?.message ?? ""
             ]
         }
@@ -338,6 +410,18 @@ enum DomainReportExporter {
         return "Unavailable"
     }
 
+    private static func provenanceLabel(_ provenance: SectionProvenance) -> String {
+        [
+            provenance.source,
+            provenance.provider,
+            provenance.resolver.map { "resolver=\($0)" },
+            provenance.resultSource.label.lowercased(),
+            textDateFormatter.string(from: provenance.collectedAt)
+        ]
+        .compactMap { $0 }
+        .joined(separator: " | ")
+    }
+
     private static let textDateFormatter: DateFormatter = {
         let formatter = DateFormatter()
         formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
diff --git a/LookupSnapshot.swift b/LookupSnapshot.swift
index dd1e5f4..f51f546 100644
--- a/LookupSnapshot.swift
+++ b/LookupSnapshot.swift
@@ -5,8 +5,20 @@ struct LookupSnapshot {
     let domain: String
     let timestamp: Date
     let trackedDomainID: UUID?
+    let note: String?
+    let appVersion: String
     let resolverDisplayName: String
     let resolverURLString: String
+    let dataSources: [String]
+    let provenanceBySection: [LookupSectionKind: SectionProvenance]
+    let availabilityConfidence: ConfidenceLevel?
+    let ownershipConfidence: ConfidenceLevel?
+    let subdomainConfidence: ConfidenceLevel?
+    let emailSecurityConfidence: ConfidenceLevel?
+    let geolocationConfidence: ConfidenceLevel?
+    let errorDetails: [LookupSectionKind: InspectionFailure]
+    let isPartialSnapshot: Bool
+    let validationIssues: [String]
     let totalLookupDurationMs: Int?
     let dnsSections: [DNSSection]
     let dnsError: String?
@@ -55,8 +67,20 @@ extension HistoryEntry {
             domain: domain,
             timestamp: timestamp,
             trackedDomainID: trackedDomainID,
+            note: note,
+            appVersion: appVersion,
             resolverDisplayName: resolverDisplayName,
             resolverURLString: resolverURLString,
+            dataSources: dataSources,
+            provenanceBySection: provenanceBySection,
+            availabilityConfidence: availabilityConfidence,
+            ownershipConfidence: ownershipConfidence,
+            subdomainConfidence: subdomainConfidence,
+            emailSecurityConfidence: emailSecurityConfidence,
+            geolocationConfidence: geolocationConfidence,
+            errorDetails: errorDetails,
+            isPartialSnapshot: isPartialSnapshot,
+            validationIssues: validationIssues,
             totalLookupDurationMs: totalLookupDurationMs,
             dnsSections: dnsSections,
             dnsError: nil,
@@ -89,7 +113,7 @@ extension HistoryEntry {
             portScanResults: portScanResults,
             portScanError: portScanError,
             changeSummary: changeSummary,
-            resultSource: .snapshot,
+            resultSource: resultSource,
             cachedSections: [],
             statusMessage: nil
         )