krz/domain-dig

an ios app for DNS & SSL analysis

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

ceac99c05f180d9008b75c9329822466ff60255b

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-04-22T16:28:28Z

feat(v2.9.0): add risk scoring and deterministic insight engine

* introduce domain risk assessment with transparent factors
* add insight engine for actionable observations
* implement cross-domain insights for workflows
* improve DNS, subdomain, email, and TLS interpretation
* classify change impact severity
* include insights and risk in export
 DomainDig.xcodeproj/project.pbxproj   |   8 +-
 DomainDig/AppVersion.swift            |   2 +-
 DomainDig/BatchResultsView.swift      |  15 +-
 DomainDig/BatchSweepSummaryView.swift |   7 +-
 DomainDig/ContentView.swift           | 204 ++++++++++++-
 DomainDig/DomainDiffService.swift     |  20 +-
 DomainDig/DomainInsightEngine.swift   | 542 ++++++++++++++++++++++++++++++++++
 DomainDig/DomainViewModel.swift       | 116 ++++++--
 DomainDig/HistoryView.swift           |  20 +-
 DomainDig/Models.swift                |  31 +-
 DomainDig/WorkflowsView.swift         |  20 ++
 DomainReportBuilder.swift             |  40 ++-
 DomainReportExporter.swift            |  78 ++++-
 13 files changed, 1052 insertions(+), 51 deletions(-)

diff --git a/DomainDig.xcodeproj/project.pbxproj b/DomainDig.xcodeproj/project.pbxproj
index 345e575..fc06760 100644
--- a/DomainDig.xcodeproj/project.pbxproj
+++ b/DomainDig.xcodeproj/project.pbxproj
@@ -354,7 +354,7 @@
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 21;
+				CURRENT_PROJECT_VERSION = 22;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				ENABLE_PREVIEWS = YES;
 				GENERATE_INFOPLIST_FILE = YES;
@@ -371,7 +371,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 2.8.0;
+				MARKETING_VERSION = 2.9.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 = 21;
+				CURRENT_PROJECT_VERSION = 22;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				ENABLE_PREVIEWS = YES;
 				GENERATE_INFOPLIST_FILE = YES;
@@ -407,7 +407,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 2.8.0;
+				MARKETING_VERSION = 2.9.0;
 				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				STRING_CATALOG_GENERATE_SYMBOLS = YES;
diff --git a/DomainDig/AppVersion.swift b/DomainDig/AppVersion.swift
index fb7481e..9342324 100644
--- a/DomainDig/AppVersion.swift
+++ b/DomainDig/AppVersion.swift
@@ -2,6 +2,6 @@ import Foundation
 
 enum AppVersion {
     static var current: String {
-        "2.8.0"
+        "2.9.0"
     }
 }
diff --git a/DomainDig/BatchResultsView.swift b/DomainDig/BatchResultsView.swift
index 51f6415..c8dde42 100644
--- a/DomainDig/BatchResultsView.swift
+++ b/DomainDig/BatchResultsView.swift
@@ -73,6 +73,9 @@ struct BatchResultRowView: View {
 
             HStack(spacing: 10) {
                 AppStatusBadgeView(model: AppStatusFactory.availability(result.availability))
+                if let riskScore = result.riskScore, let riskLevel = result.riskLevel {
+                    Text("Risk \(riskScore) \(riskLevel.title)")
+                }
                 Text(result.primaryIP ?? "No IP")
                 Text(result.timestamp.formatted(date: .abbreviated, time: .shortened))
             }
@@ -85,6 +88,12 @@ struct BatchResultRowView: View {
                     .foregroundStyle(.secondary)
             }
 
+            if let changeClassification = result.changeClassification {
+                Text("Impact: \(changeClassification.title)")
+                    .font(appDensity.font(.caption2))
+                    .foregroundStyle(changeClassification == .critical ? .red : (changeClassification == .warning ? .yellow : .secondary))
+            }
+
             if let errorMessage = result.errorMessage {
                 Text(errorMessage)
                     .font(appDensity.font(.caption2))
@@ -114,10 +123,10 @@ struct BatchResultRowView: View {
         case .running:
             return .init(title: "Running", systemImage: "arrow.clockwise", foregroundColor: .cyan, backgroundColor: .cyan.opacity(0.16))
         case .completed:
-            if result.changeSeverity == .high || result.certificateWarningLevel == .critical {
-                return .init(title: "High", systemImage: "exclamationmark.octagon.fill", foregroundColor: .red, backgroundColor: .red.opacity(0.16))
+            if result.changeClassification == .critical || result.certificateWarningLevel == .critical || result.riskLevel == .high {
+                return .init(title: "Critical", systemImage: "exclamationmark.octagon.fill", foregroundColor: .red, backgroundColor: .red.opacity(0.16))
             }
-            if result.changeSeverity == .medium || result.certificateWarningLevel == .warning {
+            if result.changeClassification == .warning || result.changeSeverity == .medium || result.certificateWarningLevel == .warning {
                 return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: .yellow, backgroundColor: .yellow.opacity(0.16))
             }
             if result.quickStatus == "Changed" {
diff --git a/DomainDig/BatchSweepSummaryView.swift b/DomainDig/BatchSweepSummaryView.swift
index 61b3e37..43ffa74 100644
--- a/DomainDig/BatchSweepSummaryView.swift
+++ b/DomainDig/BatchSweepSummaryView.swift
@@ -11,12 +11,7 @@ struct BatchSweepSummaryView: View {
             return summary.results
         }
 
-        return summary.results.filter {
-            $0.quickStatus == "Changed" ||
-                $0.quickStatus == "High" ||
-                $0.certificateWarningLevel != .none ||
-                $0.status == .failed
-        }
+        return summary.results.filter(\.hasMeaningfulChange)
     }
 
     var body: some View {
diff --git a/DomainDig/ContentView.swift b/DomainDig/ContentView.swift
index b023547..79ecf64 100644
--- a/DomainDig/ContentView.swift
+++ b/DomainDig/ContentView.swift
@@ -58,6 +58,12 @@ struct ContentView: View {
                         if viewModel.resultsLoaded {
                             SummaryView(fields: viewModel.summaryFields)
                                 .padding(.top, appDensity.metrics.cardSpacing)
+                            if let report = viewModel.currentReport {
+                                RiskSummaryCardView(report: report)
+                                    .padding(.top, appDensity.metrics.cardSpacing)
+                                InsightsSummaryCardView(insights: report.insights)
+                                    .padding(.top, appDensity.metrics.cardSpacing)
+                            }
                             if let changeSummary = viewModel.currentChangeSummary {
                                 DomainChangeSummaryView(summary: changeSummary)
                                     .padding(.top, appDensity.metrics.cardSpacing)
@@ -114,6 +120,7 @@ struct ContentView: View {
                         SubdomainsSectionView(
                             isCollapsed: sectionCollapsedBinding(.subdomains),
                             rows: viewModel.subdomainRows,
+                            groups: viewModel.currentSubdomainGroups,
                             loading: viewModel.subdomainsLoading,
                             error: viewModel.subdomainsError,
                             provenance: viewModel.currentSnapshot.provenanceBySection[.subdomains],
@@ -133,6 +140,7 @@ struct ContentView: View {
                         DNSSectionView(
                             isCollapsed: sectionCollapsedBinding(.dns),
                             dnssecLabel: viewModel.dnssecLabel,
+                            patternSummary: viewModel.currentDNSPatterns,
                             sections: viewModel.dnsRows,
                             ptrMessage: viewModel.ptrMessage,
                             loading: viewModel.dnsLoading || viewModel.ptrLoading,
@@ -145,6 +153,7 @@ struct ContentView: View {
                             isCollapsed: sectionCollapsedBinding(.web),
                             certificateRows: viewModel.webCertificateRows,
                             sslInfo: viewModel.sslInfo,
+                            tlsSummary: viewModel.currentTLSSummary,
                             sslLoading: viewModel.sslLoading || viewModel.hstsLoading,
                             sslError: viewModel.sslError,
                             tlsProvenance: viewModel.currentSnapshot.provenanceBySection[.ssl],
@@ -163,6 +172,7 @@ struct ContentView: View {
                         EmailSectionView(
                             isCollapsed: sectionCollapsedBinding(.email),
                             rows: viewModel.emailRows,
+                            assessment: viewModel.currentEmailAssessment,
                             loading: viewModel.emailSecurityLoading,
                             provenance: viewModel.currentSnapshot.provenanceBySection[.emailSecurity],
                             confidence: viewModel.currentSnapshot.emailSecurityConfidence,
@@ -654,6 +664,107 @@ struct SummaryView: View {
     }
 }
 
+struct RiskSummaryCardView: View {
+    @Environment(\.appDensity) private var appDensity
+    let report: DomainReport
+
+    private var topFactors: [RiskFactor] {
+        Array(report.riskAssessment.factors.prefix(3))
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: appDensity.metrics.cardSpacing) {
+            SectionTitleView(title: "Risk")
+            CardView(allowsHorizontalScroll: false) {
+                HStack(alignment: .firstTextBaseline) {
+                    VStack(alignment: .leading, spacing: 4) {
+                        Text("\(report.riskAssessment.score)")
+                            .font(appDensity.font(.largeTitle, weight: .bold))
+                            .foregroundStyle(levelColor)
+                        Text(report.riskAssessment.level.title)
+                            .font(appDensity.font(.caption))
+                            .foregroundStyle(levelColor)
+                    }
+                    Spacer()
+                    Text("Deterministic")
+                        .font(appDensity.font(.caption2))
+                        .foregroundStyle(.secondary)
+                }
+
+                if topFactors.isEmpty {
+                    Text("No major risk factors identified")
+                        .font(appDensity.font(.caption))
+                        .foregroundStyle(.secondary)
+                } else {
+                    ForEach(Array(topFactors.enumerated()), id: \.offset) { _, factor in
+                        HStack(alignment: .top, spacing: 8) {
+                            Circle()
+                                .fill(factorColor(factor.impact))
+                                .frame(width: 8, height: 8)
+                                .padding(.top, 5)
+                            Text(factor.description)
+                                .font(appDensity.font(.caption))
+                                .foregroundStyle(.primary)
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private var levelColor: Color {
+        switch report.riskAssessment.level {
+        case .low:
+            return .green
+        case .medium:
+            return .yellow
+        case .high:
+            return .red
+        }
+    }
+
+    private func factorColor(_ impact: RiskImpact) -> Color {
+        switch impact {
+        case .positive:
+            return .green
+        case .neutral:
+            return .secondary
+        case .negative:
+            return .red
+        }
+    }
+}
+
+struct InsightsSummaryCardView: View {
+    @Environment(\.appDensity) private var appDensity
+    let insights: [String]
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: appDensity.metrics.cardSpacing) {
+            SectionTitleView(title: "Insights")
+            CardView(allowsHorizontalScroll: false) {
+                if insights.isEmpty {
+                    Text("No deterministic insights triggered")
+                        .font(appDensity.font(.caption))
+                        .foregroundStyle(.secondary)
+                } else {
+                    ForEach(Array(insights.enumerated()), id: \.offset) { _, insight in
+                        HStack(alignment: .top, spacing: 8) {
+                            Image(systemName: "sparkline")
+                                .font(appDensity.font(.caption2))
+                                .foregroundStyle(.cyan)
+                                .padding(.top, 2)
+                            Text(insight)
+                                .font(appDensity.font(.caption))
+                                .foregroundStyle(.primary)
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
 struct StickyLookupSummaryView: View {
     @Environment(\.appDensity) private var appDensity
 
@@ -791,6 +902,13 @@ struct DomainChangeSummaryView: View {
                     .padding(.vertical, 4)
                     .background((summary.hasChanges ? severityColor(summary.severity) : .secondary).opacity(0.16))
                     .clipShape(Capsule())
+                Text(summary.impactClassification.title.uppercased())
+                    .font(appDensity.font(.caption2))
+                    .foregroundStyle(impactColor(summary.impactClassification))
+                    .padding(.horizontal, 8)
+                    .padding(.vertical, 4)
+                    .background(impactColor(summary.impactClassification).opacity(0.16))
+                    .clipShape(Capsule())
                 Text(summary.generatedAt, style: .time)
                     .font(appDensity.font(.caption2))
                     .foregroundStyle(.secondary)
@@ -822,6 +940,12 @@ struct DomainChangeSummaryView: View {
                             }
                         }
 
+                        if let riskScoreDelta = summary.riskScoreDelta {
+                            Text("Risk delta: \(riskScoreDelta >= 0 ? "+" : "")\(riskScoreDelta)")
+                                .font(appDensity.font(.caption2))
+                                .foregroundStyle(riskScoreDelta > 0 ? .orange : .secondary)
+                        }
+
                         if let contextNote = summary.contextNote {
                             Text(contextNote)
                                 .font(appDensity.font(.caption2))
@@ -846,6 +970,17 @@ struct DomainChangeSummaryView: View {
             return .red
         }
     }
+
+    private func impactColor(_ impact: ChangeImpactClassification) -> Color {
+        switch impact {
+        case .informational:
+            return .secondary
+        case .warning:
+            return .yellow
+        case .critical:
+            return .red
+        }
+    }
 }
 
 struct DomainDiffView: View {
@@ -1215,6 +1350,7 @@ struct SubdomainsSectionView: View {
     @Environment(\.appDensity) private var appDensity
     @Binding var isCollapsed: Bool
     let rows: [SubdomainRowViewData]
+    let groups: [SubdomainGroup]
     let loading: Bool
     let error: String?
     let provenance: SectionProvenance?
@@ -1235,6 +1371,22 @@ struct SubdomainsSectionView: View {
                             .padding(.top, 4)
                     }
                 } else {
+                    if !groups.isEmpty {
+                        Text("Groups")
+                            .font(appDensity.font(.caption2))
+                            .foregroundStyle(.secondary)
+                        ForEach(groups) { group in
+                            HStack {
+                                Text("\(group.label).*")
+                                    .font(appDensity.font(.caption))
+                                    .foregroundStyle(.cyan)
+                                Spacer()
+                                Text("\(group.subdomains.count)")
+                                    .font(appDensity.font(.caption2))
+                                    .foregroundStyle(.secondary)
+                            }
+                        }
+                    }
                     ForEach(rows) { row in
                         HStack(spacing: 8) {
                             Text(row.hostname)
@@ -1266,6 +1418,7 @@ struct SubdomainsSectionView: View {
 struct DNSSectionView: View {
     @Binding var isCollapsed: Bool
     let dnssecLabel: String?
+    let patternSummary: DNSPatternSummary?
     let sections: [DNSRecordSectionViewData]
     let ptrMessage: SectionMessageViewData?
     let loading: Bool
@@ -1283,6 +1436,16 @@ struct DNSSectionView: View {
                 if dnsProvenance != nil {
                     CardView(allowsHorizontalScroll: false) {
                         SectionTrustMetadataView(provenance: dnsProvenance, confidence: nil)
+                        if let patternSummary {
+                            if !patternSummary.providers.isEmpty {
+                                MessageRowView(text: "Providers: \(patternSummary.providers.joined(separator: ", "))", isError: false)
+                            }
+                            if !patternSummary.patterns.isEmpty {
+                                ForEach(Array(patternSummary.patterns.enumerated()), id: \.offset) { _, pattern in
+                                    MessageRowView(text: pattern, isError: false)
+                                }
+                            }
+                        }
                     }
                 }
                 ForEach(sections) { section in
@@ -1332,6 +1495,7 @@ struct WebSectionView: View {
     @Binding var isCollapsed: Bool
     let certificateRows: [InfoRowViewData]
     let sslInfo: SSLCertificateInfo?
+    let tlsSummary: WebResultSummary?
     let sslLoading: Bool
     let sslError: String?
     let tlsProvenance: SectionProvenance?
@@ -1354,9 +1518,17 @@ struct WebSectionView: View {
                         .font(appDensity.font(.subheadline, weight: .semibold))
                         .foregroundStyle(.cyan)
                     Spacer()
-                    AppStatusBadgeView(model: AppStatusFactory.tls(sslInfo: sslInfo, error: sslError))
+                    if !sslLoading {
+                        AppStatusBadgeView(model: AppStatusFactory.tls(sslInfo: sslInfo, error: sslError))
+                    }
                 }
                 SectionTrustMetadataView(provenance: tlsProvenance, confidence: nil)
+                if !sslLoading, let tlsSummary {
+                    LabeledValueRow(row: InfoRowViewData(label: "TLS Grade", value: tlsSummary.tlsGrade.rawValue, tone: tlsSummary.tlsGrade == .a ? .success : (tlsSummary.tlsGrade == .f ? .failure : .warning)))
+                    ForEach(Array(tlsSummary.tlsHighlights.enumerated()), id: \.offset) { _, highlight in
+                        MessageRowView(text: highlight, isError: isTLSHighlightError(highlight))
+                    }
+                }
                 if sslLoading {
                     ProgressView("Checking certificate…")
                         .appLoadingStyle()
@@ -1371,9 +1543,11 @@ struct WebSectionView: View {
                             .font(appDensity.font(.caption2))
                             .foregroundStyle(.secondary)
                         ForEach(sslInfo.subjectAltNames, id: \.self) { san in
-                            HStack(spacing: 8) {
+                            HStack(alignment: .top, spacing: 8) {
                                 Text(san)
                                     .font(appDensity.font(.caption))
+                                    .lineLimit(nil)
+                                    .fixedSize(horizontal: false, vertical: true)
                                     .textSelection(.enabled)
                                 Spacer()
                                 AppCopyButton(value: san, label: "Copy certificate SAN")
@@ -1462,12 +1636,24 @@ struct WebSectionView: View {
             }
         }
     }
+
+    private func isTLSHighlightError(_ highlight: String) -> Bool {
+        let normalized = highlight.lowercased()
+        if normalized.contains("no weak tls indicators were detected") {
+            return false
+        }
+        return normalized.contains("expires")
+            || normalized.contains("weak")
+            || normalized.contains("tls 1.0")
+            || normalized.contains("tls 1.1")
+    }
 }
 
 struct EmailSectionView: View {
     @Environment(\.appDensity) private var appDensity
     @Binding var isCollapsed: Bool
     let rows: [EmailRowViewData]
+    let assessment: EmailSecuritySummary?
     let loading: Bool
     let provenance: SectionProvenance?
     let confidence: ConfidenceLevel?
@@ -1482,6 +1668,14 @@ struct EmailSectionView: View {
                     AppStatusBadgeView(model: AppStatusFactory.email(nil, error: error))
                         .opacity(loading ? 0 : 1)
                 }
+                if let assessment, let grade = assessment.grade {
+                    LabeledValueRow(row: InfoRowViewData(label: "Grade", value: grade.rawValue, tone: grade == .a ? .success : (grade == .f ? .failure : .warning)))
+                    if !assessment.reasons.isEmpty {
+                        Text(assessment.reasons.joined(separator: " | "))
+                            .font(appDensity.font(.caption2))
+                            .foregroundStyle(.secondary)
+                    }
+                }
                 if loading {
                     ProgressView("Checking email records…")
                         .appLoadingStyle()
@@ -1817,6 +2011,8 @@ struct MessageRowView: View {
         Label(text, systemImage: isError ? "exclamationmark.triangle.fill" : "info.circle")
             .font(appDensity.font(.caption))
             .foregroundStyle(isError ? .red : .secondary)
+            .lineLimit(nil)
+            .fixedSize(horizontal: false, vertical: true)
     }
 }
 
@@ -1890,8 +2086,12 @@ struct LabeledValueRow: View {
                     Text(row.value)
                         .font(appDensity.font(.caption))
                         .foregroundStyle(ResultColors.color(for: row.tone))
+                        .lineLimit(nil)
+                        .fixedSize(horizontal: false, vertical: true)
                         .textSelection(.enabled)
                 }
+                .frame(maxWidth: .infinity, alignment: .leading)
+                .layoutPriority(1)
                 Spacer(minLength: 6)
                 if !row.value.isEmpty, row.value != "Unavailable" {
                     AppCopyButton(value: row.value, label: "Copy \(row.label)")
diff --git a/DomainDig/DomainDiffService.swift b/DomainDig/DomainDiffService.swift
index 528841d..0d8795a 100644
--- a/DomainDig/DomainDiffService.swift
+++ b/DomainDig/DomainDiffService.swift
@@ -57,7 +57,9 @@ enum DomainDiffService {
     static func summary(
         from oldSnapshot: LookupSnapshot,
         to newSnapshot: LookupSnapshot,
-        generatedAt: Date = Date()
+        generatedAt: Date = Date(),
+        riskAssessment: DomainRiskAssessment? = nil,
+        insights: [String]? = nil
     ) -> DomainChangeSummary {
         let sections = diff(from: oldSnapshot, to: newSnapshot)
         let allChangedItems = sections
@@ -70,16 +72,30 @@ enum DomainDiffService {
         let observedFacts = observedFacts(from: allChangedItems)
         let inferredConclusions = highlights.isEmpty ? [] : [message]
         let contextNote = comparisonContextNote(from: oldSnapshot, to: newSnapshot)
+        let newAnalysis = DomainInsightEngine.analyze(snapshot: newSnapshot, previousSnapshot: oldSnapshot)
+        let currentRiskAssessment = riskAssessment ?? newAnalysis.riskAssessment
+        let currentInsights = insights ?? newAnalysis.insights
+        let oldRiskAssessment = DomainInsightEngine.analyze(snapshot: oldSnapshot).riskAssessment
+        let riskScoreDelta = currentRiskAssessment.score - oldRiskAssessment.score
+        let impactClassification = DomainInsightEngine.impactClassification(
+            severity: severity,
+            riskDelta: riskScoreDelta,
+            changedSections: highlights
+        )
 
         return DomainChangeSummary(
             hasChanges: !allChangedItems.isEmpty,
             changedSections: highlights,
             message: message,
             severity: severity,
+            impactClassification: impactClassification,
             generatedAt: generatedAt,
             observedFacts: observedFacts,
             inferredConclusions: inferredConclusions,
-            contextNote: contextNote
+            contextNote: contextNote,
+            riskAssessment: currentRiskAssessment,
+            insights: currentInsights,
+            riskScoreDelta: riskScoreDelta
         )
     }
 
diff --git a/DomainDig/DomainInsightEngine.swift b/DomainDig/DomainInsightEngine.swift
new file mode 100644
index 0000000..2547727
--- /dev/null
+++ b/DomainDig/DomainInsightEngine.swift
@@ -0,0 +1,542 @@
+import Foundation
+
+enum RiskLevel: String, Codable {
+    case low
+    case medium
+    case high
+
+    var title: String { rawValue.capitalized }
+}
+
+enum RiskImpact: String, Codable {
+    case positive
+    case neutral
+    case negative
+}
+
+struct RiskFactor: Codable, Equatable {
+    let description: String
+    let impact: RiskImpact
+}
+
+struct DomainRiskAssessment: Codable, Equatable {
+    let score: Int
+    let level: RiskLevel
+    let factors: [RiskFactor]
+}
+
+enum ChangeImpactClassification: String, Codable {
+    case informational
+    case warning
+    case critical
+
+    var title: String { rawValue.capitalized }
+}
+
+enum EmailSecurityGrade: String, Codable {
+    case a = "A"
+    case b = "B"
+    case c = "C"
+    case f = "F"
+}
+
+struct EmailSecurityAssessment: Codable, Equatable {
+    let grade: EmailSecurityGrade
+    let reasons: [String]
+}
+
+enum TLSGrade: String, Codable {
+    case a = "A"
+    case b = "B"
+    case c = "C"
+    case f = "F"
+}
+
+struct TLSSummaryAssessment: Codable, Equatable {
+    let grade: TLSGrade
+    let highlights: [String]
+}
+
+struct DNSPatternSummary: Codable, Equatable {
+    let providers: [String]
+    let wildcardDetected: Bool
+    let patterns: [String]
+}
+
+struct SubdomainGroup: Codable, Equatable, Identifiable {
+    let label: String
+    let subdomains: [String]
+
+    var id: String { label }
+}
+
+struct WorkflowInsight: Codable, Equatable, Identifiable {
+    let description: String
+    let domainsInvolved: [String]
+
+    var id: String {
+        ([description] + domainsInvolved.sorted()).joined(separator: "|")
+    }
+}
+
+struct DomainAnalysisBundle {
+    let riskAssessment: DomainRiskAssessment
+    let insights: [String]
+    let dnsPatterns: DNSPatternSummary
+    let emailAssessment: EmailSecurityAssessment?
+    let tlsAssessment: TLSSummaryAssessment
+    let subdomainGroups: [SubdomainGroup]
+}
+
+enum DomainInsightEngine {
+    static func analyze(snapshot: LookupSnapshot, previousSnapshot: LookupSnapshot? = nil) -> DomainAnalysisBundle {
+        let dnsPatterns = dnsPatterns(for: snapshot)
+        let emailAssessment = emailAssessment(for: snapshot.emailSecurity)
+        let tlsAssessment = tlsAssessment(for: snapshot)
+        let subdomainGroups = groupedSubdomains(from: snapshot.subdomains.map(\.hostname))
+        let riskAssessment = riskAssessment(
+            for: snapshot,
+            previousSnapshot: previousSnapshot,
+            dnsPatterns: dnsPatterns,
+            emailAssessment: emailAssessment,
+            tlsAssessment: tlsAssessment,
+            subdomainGroups: subdomainGroups
+        )
+        let insights = insights(
+            for: snapshot,
+            previousSnapshot: previousSnapshot,
+            dnsPatterns: dnsPatterns,
+            emailAssessment: emailAssessment,
+            tlsAssessment: tlsAssessment,
+            subdomainGroups: subdomainGroups
+        )
+
+        return DomainAnalysisBundle(
+            riskAssessment: riskAssessment,
+            insights: insights,
+            dnsPatterns: dnsPatterns,
+            emailAssessment: emailAssessment,
+            tlsAssessment: tlsAssessment,
+            subdomainGroups: subdomainGroups
+        )
+    }
+
+    static func workflowInsights(for reports: [DomainReport]) -> [WorkflowInsight] {
+        var insights: [WorkflowInsight] = []
+
+        appendSharedInsights(
+            title: "Shared IP address observed",
+            groups: groupedDomains(for: reports, keyPath: \.dns.primaryIP),
+            into: &insights
+        )
+        appendSharedInsights(
+            title: "Shared nameserver set detected",
+            groups: groupedDomains(for: reports) {
+                let value = $0.ownership?.nameservers.sorted().joined(separator: "|")
+                return value?.nilIfEmpty
+            },
+            into: &insights
+        )
+        appendSharedInsights(
+            title: "Shared registrar detected",
+            groups: groupedDomains(for: reports) { $0.ownership?.registrar?.nilIfEmpty },
+            into: &insights
+        )
+        appendSharedInsights(
+            title: "Shared TLS issuer detected",
+            groups: groupedDomains(for: reports) { $0.web.tls?.issuer.nilIfEmpty },
+            into: &insights
+        )
+
+        return insights
+    }
+
+    static func impactClassification(
+        severity: ChangeSeverity,
+        riskDelta: Int,
+        changedSections: [String]
+    ) -> ChangeImpactClassification {
+        if severity == .high || riskDelta >= 20 || changedSections.contains(where: {
+            $0.localizedCaseInsensitiveContains("availability")
+                || $0.localizedCaseInsensitiveContains("certificate expires")
+        }) {
+            return .critical
+        }
+        if severity == .medium || riskDelta >= 8 || !changedSections.isEmpty {
+            return .warning
+        }
+        return .informational
+    }
+
+    private static func riskAssessment(
+        for snapshot: LookupSnapshot,
+        previousSnapshot: LookupSnapshot?,
+        dnsPatterns: DNSPatternSummary,
+        emailAssessment: EmailSecurityAssessment?,
+        tlsAssessment: TLSSummaryAssessment,
+        subdomainGroups: [SubdomainGroup]
+    ) -> DomainRiskAssessment {
+        var score = 18
+        var factors: [RiskFactor] = []
+
+        switch snapshot.availabilityResult?.status ?? .unknown {
+        case .available:
+            score -= 12
+            factors.append(.init(description: "Domain appears available rather than actively deployed", impact: .positive))
+        case .unknown:
+            score += 8
+            factors.append(.init(description: "Ownership and availability could not be confirmed", impact: .negative))
+        case .registered:
+            if !snapshot.dnsSections.isEmpty || snapshot.sslInfo != nil || !snapshot.redirectChain.isEmpty {
+                score += 6
+                factors.append(.init(description: "Registered domain exposes active infrastructure", impact: .negative))
+            } else {
+                factors.append(.init(description: "Registered domain with limited active surface detected", impact: .neutral))
+            }
+        }
+
+        let recordTypes = snapshot.dnsSections.filter { !$0.records.isEmpty || !$0.wildcardRecords.isEmpty }.count
+        if recordTypes >= 5 {
+            score += 8
+            factors.append(.init(description: "DNS configuration is broad across multiple record types", impact: .negative))
+        }
+        if dnsPatterns.wildcardDetected {
+            score += 12
+            factors.append(.init(description: "Wildcard DNS is enabled", impact: .negative))
+        }
+        if let firstPattern = dnsPatterns.patterns.first {
+            factors.append(.init(description: firstPattern, impact: .neutral))
+        }
+
+        switch tlsAssessment.grade {
+        case .a:
+            score -= 8
+            factors.append(.init(description: "TLS configuration looks current and stable", impact: .positive))
+        case .b:
+            score -= 3
+            factors.append(.init(description: "TLS is valid with minor concerns", impact: .positive))
+        case .c:
+            score += 10
+            factors.append(.init(description: "TLS configuration has visible weaknesses", impact: .negative))
+        case .f:
+            score += 22
+            factors.append(.init(description: "TLS is missing, invalid, or near failure", impact: .negative))
+        }
+
+        if let daysUntilExpiry = snapshot.sslInfo?.daysUntilExpiry, daysUntilExpiry <= 14 {
+            score += 10
+            factors.append(.init(description: "Certificate expires within 14 days", impact: .negative))
+        }
+
+        if snapshot.redirectChain.count >= 3 {
+            score += 8
+            factors.append(.init(description: "Redirect chain is longer than expected", impact: .negative))
+        }
+
+        if redirectLooksSensitive(snapshot.redirectChain.last?.url) {
+            score += 6
+            factors.append(.init(description: "Redirect target looks like an auth or account gateway", impact: .negative))
+        }
+
+        if let emailAssessment {
+            switch emailAssessment.grade {
+            case .a:
+                score -= 10
+                factors.append(.init(description: "Email protections are strong and aligned", impact: .positive))
+            case .b:
+                score -= 4
+                factors.append(.init(description: "Email protections are present with minor gaps", impact: .positive))
+            case .c:
+                score += 8
+                factors.append(.init(description: "Email protections are partial", impact: .negative))
+            case .f:
+                score += 18
+                factors.append(.init(description: "Email security protections are weak or absent", impact: .negative))
+            }
+        } else if hasMXRecords(snapshot) {
+            score += 14
+            factors.append(.init(description: "Mail is configured without enough email security evidence", impact: .negative))
+        }
+
+        let openPorts = snapshot.portScanResults.filter(\.open).map(\.port)
+        let sensitivePorts: Set<UInt16> = [21, 22, 23, 25, 3389, 5900]
+        let exposedSensitivePorts = openPorts.filter { sensitivePorts.contains($0) }
+        if !exposedSensitivePorts.isEmpty {
+            score += min(18, exposedSensitivePorts.count * 6)
+            factors.append(.init(description: "Sensitive management or mail ports are exposed", impact: .negative))
+        } else if Set(openPorts) == Set([80, 443]) {
+            factors.append(.init(description: "Exposure is limited to standard web ports", impact: .positive))
+        } else if openPorts.count >= 3 {
+            score += 8
+            factors.append(.init(description: "Multiple open services expand the attack surface", impact: .negative))
+        }
+
+        if !subdomainGroups.isEmpty {
+            let labels = Set(subdomainGroups.map(\.label))
+            if labels.contains("dev") || labels.contains("staging") {
+                score += 8
+                factors.append(.init(description: "Development or staging subdomains are discoverable", impact: .negative))
+            }
+            if labels.contains("admin") {
+                score += 10
+                factors.append(.init(description: "Administrative subdomains are discoverable", impact: .negative))
+            }
+            if snapshot.subdomains.count >= 8 {
+                score += 6
+                factors.append(.init(description: "Large passive subdomain footprint detected", impact: .negative))
+            }
+        }
+
+        if snapshot.ipGeolocation == nil,
+           snapshot.availabilityResult?.status == .registered,
+           snapshot.dnsSections.contains(where: { $0.recordType == .A && !$0.records.isEmpty }) {
+            score += 4
+            factors.append(.init(description: "Active host could not be geolocated", impact: .neutral))
+        }
+
+        if let previousSnapshot {
+            let previousAnalysis = analyze(snapshot: previousSnapshot)
+            let delta = score - previousAnalysis.riskAssessment.score
+            if delta >= 15 {
+                score += 4
+                factors.append(.init(description: "Observed risk has increased materially since the previous snapshot", impact: .negative))
+            }
+        }
+
+        let clampedScore = min(max(score, 0), 100)
+        let level: RiskLevel
+        switch clampedScore {
+        case 0..<35:
+            level = .low
+        case 35..<65:
+            level = .medium
+        default:
+            level = .high
+        }
+
+        return DomainRiskAssessment(score: clampedScore, level: level, factors: factors)
+    }
+
+    private static func insights(
+        for snapshot: LookupSnapshot,
+        previousSnapshot: LookupSnapshot?,
+        dnsPatterns: DNSPatternSummary,
+        emailAssessment: EmailSecurityAssessment?,
+        tlsAssessment: TLSSummaryAssessment,
+        subdomainGroups: [SubdomainGroup]
+    ) -> [String] {
+        var items: [String] = []
+
+        if let group = subdomainGroups.first(where: { $0.label == "staging" || $0.label == "dev" }) {
+            items.append("Multiple \(group.label) subdomains suggest non-production environments are exposed")
+        }
+        if subdomainGroups.contains(where: { $0.label == "admin" }) {
+            items.append("Administrative subdomains are publicly discoverable")
+        }
+        if let emailAssessment, emailAssessment.grade == .f {
+            items.append("Domain lacks email security protections")
+        } else if let emailAssessment, emailAssessment.grade == .c {
+            items.append("Email security is only partially enforced")
+        }
+        if let daysUntilExpiry = snapshot.sslInfo?.daysUntilExpiry, daysUntilExpiry <= 30 {
+            items.append("Certificate expires soon")
+        }
+        if redirectLooksSensitive(snapshot.redirectChain.last?.url) {
+            items.append("Redirect chain may indicate login gateway")
+        }
+        items.append(contentsOf: dnsPatterns.patterns)
+
+        if let tlsVersion = snapshot.sslInfo?.tlsVersion, tlsVersion == "TLS 1.0" || tlsVersion == "TLS 1.1" {
+            items.append("TLS protocol version is outdated")
+        }
+        if tlsAssessment.grade == .f, snapshot.sslInfo == nil, snapshot.availabilityResult?.status == .registered {
+            items.append("HTTPS endpoint could not be validated")
+        }
+        if let previousSnapshot,
+           let previousURL = previousSnapshot.redirectChain.last?.url,
+           let currentURL = snapshot.redirectChain.last?.url,
+           previousURL != currentURL {
+            items.append("Redirect target changed since the previous snapshot")
+        }
+
+        var deduplicated: [String] = []
+        for item in items where !deduplicated.contains(item) {
+            deduplicated.append(item)
+        }
+        return deduplicated
+    }
+
+    private static func dnsPatterns(for snapshot: LookupSnapshot) -> DNSPatternSummary {
+        let nameservers = snapshot.ownership?.nameservers.map { $0.lowercased() } ?? []
+        let headerNames = Set(snapshot.httpHeaders.map { $0.name.lowercased() })
+        let headerValues = snapshot.httpHeaders.map { $0.value.lowercased() }
+        let allValues = snapshot.dnsSections.flatMap { section in
+            (section.records + section.wildcardRecords).map { $0.value.lowercased() }
+        }
+
+        var providers: [String] = []
+        if nameservers.contains(where: { $0.contains("cloudflare") }) || headerNames.contains("cf-ray") || headerNames.contains("cf-cache-status") {
+            providers.append("Cloudflare")
+        }
+        if nameservers.contains(where: { $0.contains("awsdns") }) || allValues.contains(where: { $0.contains("cloudfront.net") || $0.contains("elb.amazonaws.com") || $0.contains("amazonaws.com") }) {
+            providers.append("AWS")
+        }
+        if allValues.contains(where: { $0.contains("fastly.net") }) || headerValues.contains(where: { $0.contains("fastly") || $0.contains("cache-") }) {
+            providers.append("Fastly")
+        }
+
+        var patterns: [String] = []
+        let wildcardDetected = snapshot.dnsSections.contains { !$0.wildcardRecords.isEmpty }
+        if !providers.isEmpty {
+            patterns.append("CDN or edge network detected: \(providers.joined(separator: ", "))")
+        }
+        if wildcardDetected {
+            patterns.append("Wildcard DNS responses are present")
+        }
+        if hasMXRecords(snapshot), snapshot.emailSecurity == nil {
+            patterns.append("MX records exist without corresponding email security records")
+        }
+        if snapshot.sslInfo != nil && !(snapshot.dnsSections.first(where: { $0.recordType == .CAA })?.records.isEmpty == false) {
+            patterns.append("TLS is active but no CAA record was found")
+        }
+
+        return DNSPatternSummary(providers: providers, wildcardDetected: wildcardDetected, patterns: patterns)
+    }
+
+    private static func emailAssessment(for result: EmailSecurityResult?) -> EmailSecurityAssessment? {
+        guard let result else { return nil }
+
+        let spfFound = result.spf.found
+        let dkimFound = result.dkim.found
+        let dmarcFound = result.dmarc.found
+        let dmarcStrict = isStrictDMARC(result.dmarc.value)
+
+        let reasons = [
+            spfFound ? "SPF present" : "SPF missing",
+            dmarcFound ? (dmarcStrict ? "DMARC policy is strict" : "DMARC policy is not strict") : "DMARC missing",
+            dkimFound ? "DKIM present" : "DKIM not detected"
+        ]
+
+        let grade: EmailSecurityGrade
+        if spfFound && dkimFound && dmarcStrict {
+            grade = .a
+        } else if spfFound && dmarcFound && (dkimFound || dmarcStrict) {
+            grade = .b
+        } else if spfFound || dmarcFound || dkimFound {
+            grade = .c
+        } else {
+            grade = .f
+        }
+
+        return EmailSecurityAssessment(grade: grade, reasons: reasons)
+    }
+
+    private static func tlsAssessment(for snapshot: LookupSnapshot) -> TLSSummaryAssessment {
+        guard let sslInfo = snapshot.sslInfo else {
+            return TLSSummaryAssessment(grade: .f, highlights: ["TLS handshake failed or no certificate was returned"])
+        }
+
+        var issues: [String] = []
+        if sslInfo.daysUntilExpiry <= 14 {
+            issues.append("Certificate expires within 14 days")
+        } else if sslInfo.daysUntilExpiry <= 30 {
+            issues.append("Certificate expires within 30 days")
+        }
+        if let tlsVersion = sslInfo.tlsVersion, tlsVersion == "TLS 1.0" || tlsVersion == "TLS 1.1" {
+            issues.append("Uses \(tlsVersion)")
+        }
+        if let cipherSuite = sslInfo.cipherSuite?.lowercased(),
+           cipherSuite.contains("_cbc_") || cipherSuite.contains("3des") || cipherSuite.contains("rc4") {
+            issues.append("Negotiated cipher suite looks weak")
+        }
+
+        let grade: TLSGrade
+        if snapshot.sslError != nil {
+            grade = .f
+        } else if issues.contains(where: { $0.contains("14 days") || $0.contains("weak") || $0.contains("TLS 1.0") || $0.contains("TLS 1.1") }) {
+            grade = .c
+        } else if !issues.isEmpty {
+            grade = .b
+        } else {
+            grade = .a
+        }
+
+        return TLSSummaryAssessment(
+            grade: grade,
+            highlights: issues.isEmpty ? ["Certificate is valid and no weak TLS indicators were detected"] : issues
+        )
+    }
+
+    private static func groupedSubdomains(from subdomains: [String]) -> [SubdomainGroup] {
+        let labels = ["api", "dev", "staging", "admin"]
+        let normalized = Array(Set(subdomains.map { $0.lowercased() })).sorted()
+
+        return labels.compactMap { label in
+            let matches = normalized.filter {
+                guard let firstLabel = $0.split(separator: ".").first?.lowercased() else { return false }
+                return firstLabel == label
+            }
+            guard !matches.isEmpty else { return nil }
+            return SubdomainGroup(label: label, subdomains: matches)
+        }
+    }
+
+    private static func isStrictDMARC(_ value: String?) -> Bool {
+        guard let value = value?.lowercased() else { return false }
+        return value.contains("p=reject") || value.contains("p=quarantine")
+    }
+
+    private static func hasMXRecords(_ snapshot: LookupSnapshot) -> Bool {
+        snapshot.dnsSections.contains { $0.recordType == .MX && !$0.records.isEmpty }
+    }
+
+    private static func redirectLooksSensitive(_ urlString: String?) -> Bool {
+        guard let urlString = urlString?.lowercased() else { return false }
+        return urlString.contains("/login")
+            || urlString.contains("/signin")
+            || urlString.contains("/auth")
+            || urlString.contains("/account")
+            || urlString.contains("sso")
+    }
+
+    private static func appendSharedInsights(
+        title: String,
+        groups: [String: [String]],
+        into insights: inout [WorkflowInsight]
+    ) {
+        for domains in groups.values where domains.count >= 2 {
+            insights.append(
+                WorkflowInsight(
+                    description: "\(title) across \(domains.count) domains",
+                    domainsInvolved: domains.sorted()
+                )
+            )
+        }
+    }
+
+    private static func groupedDomains(
+        for reports: [DomainReport],
+        keyPath: KeyPath<DomainReport, String?>
+    ) -> [String: [String]] {
+        groupedDomains(for: reports) { $0[keyPath: keyPath]?.nilIfEmpty }
+    }
+
+    private static func groupedDomains(
+        for reports: [DomainReport],
+        transform: (DomainReport) -> String?
+    ) -> [String: [String]] {
+        var grouped: [String: [String]] = [:]
+        for report in reports {
+            guard let value = transform(report) else { continue }
+            grouped[value, default: []].append(report.domain)
+        }
+        return grouped
+    }
+}
+
+private extension String {
+    var nilIfEmpty: String? {
+        let trimmed = trimmingCharacters(in: .whitespacesAndNewlines)
+        return trimmed.isEmpty ? nil : trimmed
+    }
+}
diff --git a/DomainDig/DomainViewModel.swift b/DomainDig/DomainViewModel.swift
index f9a6523..18ce4c2 100644
--- a/DomainDig/DomainViewModel.swift
+++ b/DomainDig/DomainViewModel.swift
@@ -96,6 +96,13 @@ private struct BatchLookupPayload {
     let snapshot: LookupSnapshot
 }
 
+private struct WorkflowExportPayload: Codable {
+    let workflowName: String
+    let generatedAt: Date
+    let workflowInsights: [WorkflowInsight]
+    let reports: [DomainReport]
+}
+
 @MainActor
 @Observable
 final class DomainViewModel {
@@ -194,6 +201,7 @@ final class DomainViewModel {
     private(set) var currentStatusMessage: String?
     private(set) var currentSnapshotTimestamp = Date()
     private(set) var currentHistoryEntryID: UUID?
+    private(set) var currentReport: DomainReport?
 
     private static let recentSearchesKey = "recentSearches"
     private static let maxRecent = 20
@@ -448,16 +456,28 @@ final class DomainViewModel {
         return history.first(where: { $0.id == currentHistoryEntryID })
     }
 
-    var currentReport: DomainReport? {
-        guard !searchedDomain.isEmpty else { return nil }
-        return reportBuilder.build(
-            from: currentSnapshot,
-            previousSnapshot: previousSnapshot(
-                for: searchedDomain,
-                trackedDomainID: currentTrackedDomain?.id,
-                replacingLatest: false
-            )
-        )
+    var currentRiskAssessment: DomainRiskAssessment? {
+        currentReport?.riskAssessment
+    }
+
+    var currentInsights: [String] {
+        currentReport?.insights ?? []
+    }
+
+    var currentSubdomainGroups: [SubdomainGroup] {
+        currentReport?.subdomainGroups ?? []
+    }
+
+    var currentDNSPatterns: DNSPatternSummary? {
+        currentReport?.dns.patternSummary
+    }
+
+    var currentEmailAssessment: EmailSecuritySummary? {
+        currentReport?.email
+    }
+
+    var currentTLSSummary: WebResultSummary? {
+        currentReport?.web
     }
 
     var summaryFields: [SummaryFieldViewData] {
@@ -684,6 +704,7 @@ final class DomainViewModel {
         currentDiffSections = []
         currentChangeSummary = nil
         ownershipDiff = []
+        currentReport = nil
         refreshingTrackedDomainID = nil
         clearBatchState()
         clearLookupState()
@@ -728,7 +749,10 @@ final class DomainViewModel {
                 quickStatus: "Cancelled",
                 summaryMessage: batchResults[index].summaryMessage,
                 changeSeverity: batchResults[index].changeSeverity,
+                changeClassification: batchResults[index].changeClassification,
                 certificateWarningLevel: batchResults[index].certificateWarningLevel,
+                riskScore: batchResults[index].riskScore,
+                riskLevel: batchResults[index].riskLevel,
                 timestamp: Date(),
                 status: .failed,
                 errorMessage: "Lookup cancelled"
@@ -918,22 +942,36 @@ final class DomainViewModel {
     }
 
     func exportWorkflowText(summary: WorkflowRunSummary, changedOnly: Bool) -> String {
-        DomainReportExporter.batchText(
-            for: workflowReports(from: summary, changedOnly: changedOnly),
+        let reports = workflowReports(from: summary, changedOnly: changedOnly)
+        let base = DomainReportExporter.batchText(
+            for: reports,
             title: "\(summary.workflowName) Workflow Export"
         )
+        guard !summary.workflowInsights.isEmpty else { return base }
+        let insightLines = summary.workflowInsights.map {
+            "- \($0.description): \($0.domainsInvolved.joined(separator: ", "))"
+        }
+        return ([ "\(summary.workflowName) Workflow Insights", String(repeating: "-", count: 32) ] + insightLines + ["", base]).joined(separator: "\n")
     }
 
     func exportWorkflowCSV(summary: WorkflowRunSummary, changedOnly: Bool) -> String {
-        DomainReportExporter.csv(for: workflowReports(from: summary, changedOnly: changedOnly))
+        DomainReportExporter.csv(
+            for: workflowReports(from: summary, changedOnly: changedOnly),
+            workflowInsights: summary.workflowInsights
+        )
     }
 
     func exportWorkflowJSONData(summary: WorkflowRunSummary, changedOnly: Bool) -> Data? {
-        try? DomainReportExporter.data(
-            for: workflowReports(from: summary, changedOnly: changedOnly),
-            format: .json,
-            title: "\(summary.workflowName) Workflow Export"
+        let payload = WorkflowExportPayload(
+            workflowName: summary.workflowName,
+            generatedAt: summary.generatedAt,
+            workflowInsights: summary.workflowInsights,
+            reports: workflowReports(from: summary, changedOnly: changedOnly)
         )
+        let encoder = JSONEncoder()
+        encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
+        encoder.dateEncodingStrategy = .iso8601
+        return try? encoder.encode(payload)
     }
 
     private func performLookup(domain: String, lookupID: UUID) async -> HistoryEntry? {
@@ -963,9 +1001,17 @@ final class DomainViewModel {
         currentResultSource = snapshot.resultSource
         currentCachedSections = snapshot.cachedSections
         currentStatusMessage = snapshot.statusMessage
-        currentChangeSummary = snapshot.changeSummary
         currentDiffSections = []
         ownershipDiff = []
+        currentReport = reportBuilder.build(
+            from: snapshot,
+            previousSnapshot: previousSnapshot(
+                for: snapshot.domain,
+                trackedDomainID: snapshot.trackedDomainID ?? trackedDomain(for: snapshot.domain)?.id,
+                replacingLatest: false
+            )
+        )
+        currentChangeSummary = currentReport?.changeSummary ?? snapshot.changeSummary
 
         dnsSections = snapshot.dnsSections
         dnsError = snapshot.dnsError
@@ -1406,8 +1452,15 @@ final class DomainViewModel {
     private func saveHistoryEntry(from snapshot: LookupSnapshot, replaceLatest: Bool, updateCurrentState: Bool) -> HistoryEntry? {
         let trackedDomainID = snapshot.trackedDomainID ?? trackedDomain(for: snapshot.domain)?.id
         let previousSnapshot = previousSnapshot(for: snapshot.domain, trackedDomainID: trackedDomainID, replacingLatest: replaceLatest)
+        let analysis = DomainInsightEngine.analyze(snapshot: snapshot, previousSnapshot: previousSnapshot)
         let changeSummary = previousSnapshot.map {
-            DomainDiffService.summary(from: $0, to: snapshot, generatedAt: snapshot.timestamp)
+            DomainDiffService.summary(
+                from: $0,
+                to: snapshot,
+                generatedAt: snapshot.timestamp,
+                riskAssessment: analysis.riskAssessment,
+                insights: analysis.insights
+            )
         }
         let diffSections = previousSnapshot.map { DomainDiffService.diff(from: $0, to: snapshot) } ?? []
 
@@ -1415,6 +1468,7 @@ final class DomainViewModel {
             currentChangeSummary = changeSummary
             currentDiffSections = diffSections
             ownershipDiff = diffSections.first(where: { $0.title == "Ownership" })?.items.filter(\.hasChanges) ?? []
+            currentReport = reportBuilder.build(from: snapshot, previousSnapshot: previousSnapshot)
         }
 
         let entry = HistoryEntry(
@@ -1664,6 +1718,7 @@ final class DomainViewModel {
         currentStatusMessage = nil
         currentDiffSections = []
         currentChangeSummary = nil
+        currentReport = nil
         ownershipDiff = []
         clearLookupState()
         setAllLoadingStates(true)
@@ -1807,11 +1862,14 @@ final class DomainViewModel {
             }
         }
         let certificateWarningLevel = DomainDiffService.certificateWarningLevel(for: payload.snapshot)
+        let riskAssessment = DomainInsightEngine.analyze(snapshot: payload.snapshot).riskAssessment
         let quickStatus: String
         if entry?.changeSummary?.hasChanges == true {
-            quickStatus = entry?.changeSummary?.severity == .high ? "High" : "Changed"
+            quickStatus = entry?.changeSummary?.impactClassification == .critical ? "Critical" : (entry?.changeSummary?.severity == .high ? "High" : "Changed")
         } else if certificateWarningLevel != .none {
             quickStatus = certificateWarningLevel == .critical ? "Critical" : "Warning"
+        } else if riskAssessment.level == .high {
+            quickStatus = "High"
         } else {
             quickStatus = "Unchanged"
         }
@@ -1834,9 +1892,14 @@ final class DomainViewModel {
         refreshingTrackedDomainID = nil
         batchTask = nil
 
-        let changedCount = batchResults.filter { $0.quickStatus == "Changed" || $0.quickStatus == "High" }.count
+        let changedCount = batchResults.filter { $0.quickStatus == "Changed" || $0.quickStatus == "High" || $0.quickStatus == "Critical" }.count
         let unchangedCount = batchResults.filter { $0.quickStatus == "Unchanged" && $0.status == .completed }.count
-        let warningCount = batchResults.filter { $0.certificateWarningLevel != .none }.count
+        let warningCount = batchResults.filter {
+            $0.certificateWarningLevel != .none
+                || $0.changeClassification == .warning
+                || $0.changeClassification == .critical
+                || $0.riskLevel == .high
+        }.count
 
         let summary = BatchSweepSummary(
             source: source,
@@ -1855,6 +1918,10 @@ final class DomainViewModel {
         latestBatchSweepSummary = summary
 
         if source == .workflow, let activeWorkflowRunID, let activeWorkflowRunName {
+            let workflowReports: [DomainReport] = summary.results.compactMap { result in
+                guard let entry = historyEntry(for: result) else { return nil }
+                return report(for: entry)
+            }
             latestWorkflowRunSummary = WorkflowRunSummary(
                 workflowID: activeWorkflowRunID,
                 workflowName: activeWorkflowRunName,
@@ -1863,6 +1930,7 @@ final class DomainViewModel {
                 unchangedDomains: unchangedCount,
                 warningDomains: warningCount,
                 results: summary.results,
+                workflowInsights: DomainInsightEngine.workflowInsights(for: workflowReports),
                 generatedAt: summary.generatedAt
             )
         }
@@ -1896,7 +1964,10 @@ final class DomainViewModel {
             quickStatus: quickStatus,
             summaryMessage: entry?.changeSummary?.message,
             changeSeverity: entry?.changeSummary?.severity,
+            changeClassification: entry?.changeSummary?.impactClassification,
             certificateWarningLevel: entry.map { DomainDiffService.certificateWarningLevel(for: $0.snapshot) } ?? batchResults[index].certificateWarningLevel,
+            riskScore: entry.map { $0.changeSummary?.riskAssessment?.score ?? report(for: $0).riskAssessment.score },
+            riskLevel: entry.map { $0.changeSummary?.riskAssessment?.level ?? report(for: $0).riskAssessment.level },
             timestamp: entry?.timestamp ?? Date(),
             status: status,
             errorMessage: errorMessage
@@ -1956,6 +2027,7 @@ final class DomainViewModel {
         currentResultSource = .live
         currentCachedSections = []
         currentStatusMessage = nil
+        currentReport = nil
     }
 
     private func setAllLoadingStates(_ loading: Bool) {
diff --git a/DomainDig/HistoryView.swift b/DomainDig/HistoryView.swift
index 3bda59d..e3a77f4 100644
--- a/DomainDig/HistoryView.swift
+++ b/DomainDig/HistoryView.swift
@@ -167,12 +167,24 @@ struct HistoryDetailView: View {
         entry.snapshot
     }
 
+    private var report: DomainReport {
+        DomainReportBuilder().build(from: entry, previousSnapshot: viewModel.comparisonSnapshot(for: entry))
+    }
+
+    private var trackedDomain: TrackedDomain? {
+        viewModel.trackedDomains.first { $0.domain.caseInsensitiveCompare(entry.domain) == .orderedSame }
+    }
+
     var body: some View {
         ScrollView(.vertical) {
             VStack(alignment: .leading, spacing: 0) {
                 snapshotBanner
                 SummaryView(fields: DomainViewModel.summaryFields(from: snapshot))
                     .padding(.top, 8)
+                RiskSummaryCardView(report: report)
+                    .padding(.top, 8)
+                InsightsSummaryCardView(insights: report.insights)
+                    .padding(.top, 8)
                 DomainSectionView(
                     isCollapsed: .constant(false),
                     rows: DomainViewModel.domainRows(from: snapshot),
@@ -183,14 +195,14 @@ struct HistoryDetailView: View {
                     provenance: snapshot.provenanceBySection[.availability],
                     confidence: snapshot.availabilityConfidence,
                     snapshotNote: entry.note,
-                    trackedDomain: viewModel.trackedDomains.first(where: { $0.domain.lowercased() == entry.domain.lowercased() }),
+                    trackedDomain: trackedDomain,
                     workflows: viewModel.workflowsContaining(domain: entry.domain),
                     trackingLimitMessage: nil,
                     onTrack: {
                         _ = viewModel.trackDomain(domain: entry.domain, availabilityStatus: entry.availabilityResult?.status)
                     },
                     onTogglePinned: {
-                        guard let trackedDomain = viewModel.trackedDomains.first(where: { $0.domain.lowercased() == entry.domain.lowercased() }) else { return }
+                        guard let trackedDomain else { return }
                         viewModel.togglePinned(for: trackedDomain)
                     },
                     onEditNote: nil,
@@ -212,6 +224,7 @@ struct HistoryDetailView: View {
                 SubdomainsSectionView(
                     isCollapsed: .constant(false),
                     rows: DomainViewModel.subdomainRows(from: snapshot),
+                    groups: report.subdomainGroups,
                     loading: false,
                     error: snapshot.subdomainsError,
                     provenance: snapshot.provenanceBySection[.subdomains],
@@ -235,6 +248,7 @@ struct HistoryDetailView: View {
                 DNSSectionView(
                     isCollapsed: .constant(false),
                     dnssecLabel: DomainViewModel.dnssecLabel(from: snapshot),
+                    patternSummary: report.dns.patternSummary,
                     sections: DomainViewModel.dnsRows(from: snapshot),
                     ptrMessage: DomainViewModel.ptrMessage(from: snapshot),
                     loading: false,
@@ -247,6 +261,7 @@ struct HistoryDetailView: View {
                     isCollapsed: .constant(false),
                     certificateRows: DomainViewModel.webCertificateRows(from: snapshot),
                     sslInfo: snapshot.sslInfo,
+                    tlsSummary: report.web,
                     sslLoading: false,
                     sslError: snapshot.sslError,
                     tlsProvenance: snapshot.provenanceBySection[.ssl],
@@ -265,6 +280,7 @@ struct HistoryDetailView: View {
                 EmailSectionView(
                     isCollapsed: .constant(false),
                     rows: DomainViewModel.emailRows(from: snapshot),
+                    assessment: report.email,
                     loading: false,
                     provenance: snapshot.provenanceBySection[.emailSecurity],
                     confidence: snapshot.emailSecurityConfidence,
diff --git a/DomainDig/Models.swift b/DomainDig/Models.swift
index 0c2b4c2..6b7c2c1 100644
--- a/DomainDig/Models.swift
+++ b/DomainDig/Models.swift
@@ -181,29 +181,41 @@ struct DomainChangeSummary: Codable, Equatable {
     let changedSections: [String]
     let message: String
     let severity: ChangeSeverity
+    let impactClassification: ChangeImpactClassification
     let generatedAt: Date
     let observedFacts: [String]
     let inferredConclusions: [String]
     let contextNote: String?
+    let riskAssessment: DomainRiskAssessment?
+    let insights: [String]
+    let riskScoreDelta: Int?
 
     init(
         hasChanges: Bool,
         changedSections: [String],
         message: String,
         severity: ChangeSeverity,
+        impactClassification: ChangeImpactClassification,
         generatedAt: Date,
         observedFacts: [String] = [],
         inferredConclusions: [String] = [],
-        contextNote: String? = nil
+        contextNote: String? = nil,
+        riskAssessment: DomainRiskAssessment? = nil,
+        insights: [String] = [],
+        riskScoreDelta: Int? = nil
     ) {
         self.hasChanges = hasChanges
         self.changedSections = changedSections
         self.message = message
         self.severity = severity
+        self.impactClassification = impactClassification
         self.generatedAt = generatedAt
         self.observedFacts = observedFacts
         self.inferredConclusions = inferredConclusions
         self.contextNote = contextNote
+        self.riskAssessment = riskAssessment
+        self.insights = insights
+        self.riskScoreDelta = riskScoreDelta
     }
 
     init(from decoder: Decoder) throws {
@@ -212,11 +224,16 @@ struct DomainChangeSummary: Codable, Equatable {
         changedSections = try container.decodeIfPresent([String].self, forKey: .changedSections) ?? []
         generatedAt = try container.decode(Date.self, forKey: .generatedAt)
         severity = try container.decodeIfPresent(ChangeSeverity.self, forKey: .severity) ?? (hasChanges ? .medium : .low)
+        impactClassification = try container.decodeIfPresent(ChangeImpactClassification.self, forKey: .impactClassification)
+            ?? (severity == .high ? .critical : (hasChanges ? .warning : .informational))
         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)
+        riskAssessment = try container.decodeIfPresent(DomainRiskAssessment.self, forKey: .riskAssessment)
+        insights = try container.decodeIfPresent([String].self, forKey: .insights) ?? []
+        riskScoreDelta = try container.decodeIfPresent(Int.self, forKey: .riskScoreDelta)
     }
 }
 
@@ -243,7 +260,10 @@ struct BatchLookupResult: Identifiable, Codable, Equatable {
     let quickStatus: String
     let summaryMessage: String?
     let changeSeverity: ChangeSeverity?
+    let changeClassification: ChangeImpactClassification?
     let certificateWarningLevel: CertificateWarningLevel
+    let riskScore: Int?
+    let riskLevel: RiskLevel?
     let timestamp: Date
     let status: BatchLookupStatus
     let errorMessage: String?
@@ -258,7 +278,10 @@ struct BatchLookupResult: Identifiable, Codable, Equatable {
         quickStatus: String,
         summaryMessage: String? = nil,
         changeSeverity: ChangeSeverity? = nil,
+        changeClassification: ChangeImpactClassification? = nil,
         certificateWarningLevel: CertificateWarningLevel = .none,
+        riskScore: Int? = nil,
+        riskLevel: RiskLevel? = nil,
         timestamp: Date,
         status: BatchLookupStatus,
         errorMessage: String? = nil
@@ -272,7 +295,10 @@ struct BatchLookupResult: Identifiable, Codable, Equatable {
         self.quickStatus = quickStatus
         self.summaryMessage = summaryMessage
         self.changeSeverity = changeSeverity
+        self.changeClassification = changeClassification
         self.certificateWarningLevel = certificateWarningLevel
+        self.riskScore = riskScore
+        self.riskLevel = riskLevel
         self.timestamp = timestamp
         self.status = status
         self.errorMessage = errorMessage
@@ -281,7 +307,9 @@ struct BatchLookupResult: Identifiable, Codable, Equatable {
     var hasMeaningfulChange: Bool {
         quickStatus == "Changed"
             || quickStatus == "High"
+            || quickStatus == "Critical"
             || certificateWarningLevel != .none
+            || riskLevel == .high
             || status == .failed
     }
 }
@@ -331,6 +359,7 @@ struct WorkflowRunSummary: Identifiable, Equatable {
     let unchangedDomains: Int
     let warningDomains: Int
     let results: [BatchLookupResult]
+    let workflowInsights: [WorkflowInsight]
     let generatedAt: Date
 }
 
diff --git a/DomainDig/WorkflowsView.swift b/DomainDig/WorkflowsView.swift
index ba88487..b870409 100644
--- a/DomainDig/WorkflowsView.swift
+++ b/DomainDig/WorkflowsView.swift
@@ -215,6 +215,11 @@ struct WorkflowDetailView: View {
                             statRow(label: "Changed", value: "\(latestSummary.changedDomains)")
                             statRow(label: "Warnings", value: "\(latestSummary.warningDomains)")
                             statRow(label: "Unchanged", value: "\(latestSummary.unchangedDomains)")
+                            if !latestSummary.workflowInsights.isEmpty {
+                                Text(latestSummary.workflowInsights[0].description)
+                                    .font(appDensity.font(.caption))
+                                    .foregroundStyle(.secondary)
+                            }
 
                             Button {
                                 viewModel.latestWorkflowRunSummary = latestSummary
@@ -431,6 +436,21 @@ struct WorkflowRunSummaryView: View {
                     Toggle("Show unchanged domains", isOn: $showAllResults)
                 }
 
+                if !summary.workflowInsights.isEmpty {
+                    Section("Workflow Insights") {
+                        ForEach(summary.workflowInsights) { insight in
+                            VStack(alignment: .leading, spacing: 4) {
+                                Text(insight.description)
+                                    .font(.system(.callout, design: .monospaced))
+                                    .foregroundStyle(.primary)
+                                Text(insight.domainsInvolved.joined(separator: ", "))
+                                    .font(.system(.caption, design: .monospaced))
+                                    .foregroundStyle(.secondary)
+                            }
+                        }
+                    }
+                }
+
                 Section(visibleResults.isEmpty ? "Meaningful Changes" : "Results") {
                     if visibleResults.isEmpty {
                         Text("No domains with meaningful changes or warnings")
diff --git a/DomainReportBuilder.swift b/DomainReportBuilder.swift
index 765ad4f..ceda05e 100644
--- a/DomainReportBuilder.swift
+++ b/DomainReportBuilder.swift
@@ -25,6 +25,9 @@ struct DomainReport: Codable {
     let email: EmailSecuritySummary
     let network: NetworkSummary
     let subdomains: [String]
+    let subdomainGroups: [SubdomainGroup]
+    let riskAssessment: DomainRiskAssessment
+    let insights: [String]
     let changeSummary: DomainChangeSummary?
 }
 
@@ -36,6 +39,7 @@ struct DNSResultSummary: Codable {
     let primaryIP: String?
     let ptrRecord: String?
     let dnssecSigned: Bool?
+    let patternSummary: DNSPatternSummary
     let error: String?
     let ptrError: String?
 }
@@ -43,6 +47,8 @@ struct DNSResultSummary: Codable {
 struct WebResultSummary: Codable {
     let tls: SSLCertificateInfo?
     let tlsStatus: String
+    let tlsGrade: TLSGrade
+    let tlsHighlights: [String]
     let certificateWarningLevel: CertificateWarningLevel
     let hstsPreloaded: Bool?
     let headers: [HTTPHeader]
@@ -61,6 +67,8 @@ struct WebResultSummary: Codable {
 
 struct EmailSecuritySummary: Codable {
     let records: EmailSecurityResult?
+    let grade: EmailSecurityGrade?
+    let reasons: [String]
     let summary: String
     let error: String?
 }
@@ -81,6 +89,21 @@ struct NetworkSummary: Codable {
 struct DomainReportBuilder {
     func build(from snapshot: LookupSnapshot, previousSnapshot: LookupSnapshot? = nil) -> DomainReport {
         let primaryIP = primaryIPAddress(from: snapshot)
+        let analysis = DomainInsightEngine.analyze(snapshot: snapshot, previousSnapshot: previousSnapshot)
+        let changeSummary: DomainChangeSummary?
+        if let existingChangeSummary = snapshot.changeSummary, existingChangeSummary.riskAssessment != nil {
+            changeSummary = existingChangeSummary
+        } else {
+            changeSummary = previousSnapshot.map {
+                DomainDiffService.summary(
+                    from: $0,
+                    to: snapshot,
+                    generatedAt: snapshot.timestamp,
+                    riskAssessment: analysis.riskAssessment,
+                    insights: analysis.insights
+                )
+            }
+        }
 
         return DomainReport(
             domain: snapshot.domain,
@@ -110,12 +133,15 @@ struct DomainReportBuilder {
                 primaryIP: primaryIP,
                 ptrRecord: snapshot.ptrRecord,
                 dnssecSigned: dnssecSigned(from: snapshot),
+                patternSummary: analysis.dnsPatterns,
                 error: snapshot.dnsError,
                 ptrError: snapshot.ptrError
             ),
             web: WebResultSummary(
                 tls: snapshot.sslInfo,
                 tlsStatus: tlsStatus(from: snapshot),
+                tlsGrade: analysis.tlsAssessment.grade,
+                tlsHighlights: analysis.tlsAssessment.highlights,
                 certificateWarningLevel: DomainDiffService.certificateWarningLevel(for: snapshot),
                 hstsPreloaded: snapshot.hstsPreloaded,
                 headers: snapshot.httpHeaders,
@@ -133,7 +159,9 @@ struct DomainReportBuilder {
             ),
             email: EmailSecuritySummary(
                 records: snapshot.emailSecurity,
-                summary: emailSummary(from: snapshot),
+                grade: analysis.emailAssessment?.grade,
+                reasons: analysis.emailAssessment?.reasons ?? [],
+                summary: emailSummary(from: snapshot, assessment: analysis.emailAssessment),
                 error: snapshot.emailSecurityError
             ),
             network: NetworkSummary(
@@ -149,9 +177,10 @@ struct DomainReportBuilder {
                 portScanError: snapshot.portScanError
             ),
             subdomains: snapshot.subdomains.map(\.hostname),
-            changeSummary: snapshot.changeSummary ?? previousSnapshot.map {
-                DomainDiffService.summary(from: $0, to: snapshot, generatedAt: snapshot.timestamp)
-            }
+            subdomainGroups: analysis.subdomainGroups,
+            riskAssessment: analysis.riskAssessment,
+            insights: analysis.insights,
+            changeSummary: changeSummary
         )
     }
 
@@ -177,12 +206,13 @@ struct DomainReportBuilder {
         return "unavailable"
     }
 
-    private func emailSummary(from snapshot: LookupSnapshot) -> String {
+    private func emailSummary(from snapshot: LookupSnapshot, assessment: EmailSecurityAssessment?) -> String {
         guard let emailSecurity = snapshot.emailSecurity else {
             return snapshot.emailSecurityError ?? "Unavailable"
         }
 
         return [
+            "Grade \(assessment?.grade.rawValue ?? "?")",
             "SPF \(emailSecurity.spf.found ? "Yes" : "No")",
             "DMARC \(emailSecurity.dmarc.found ? "Yes" : "No")",
             "DKIM \(emailSecurity.dkim.found ? "Yes" : "No")",
diff --git a/DomainReportExporter.swift b/DomainReportExporter.swift
index 0841fcf..9b20066 100644
--- a/DomainReportExporter.swift
+++ b/DomainReportExporter.swift
@@ -60,6 +60,7 @@ enum DomainReportExporter {
         appendSection("Summary", to: &lines) {
             [
                 "Primary IP: \(report.dns.primaryIP ?? "Unavailable")",
+                "Risk Score: \(report.riskAssessment.score) (\(report.riskAssessment.level.title))",
                 "TLS Status: \(report.web.tlsStatus)",
                 "HTTP: \(httpSummary(for: report))",
                 "Email: \(report.email.summary)",
@@ -67,6 +68,26 @@ enum DomainReportExporter {
             ]
         }
 
+        appendSection("Risk", to: &lines) {
+            var values = [
+                "Score: \(report.riskAssessment.score)",
+                "Level: \(report.riskAssessment.level.title)"
+            ]
+            if report.riskAssessment.factors.isEmpty {
+                values.append("Factors: None")
+            } else {
+                values.append("Factors:")
+                for factor in report.riskAssessment.factors {
+                    values.append("  [\(factor.impact.rawValue)] \(factor.description)")
+                }
+            }
+            return values
+        }
+
+        appendSection("Insights", to: &lines) {
+            report.insights.isEmpty ? ["No deterministic insights triggered"] : report.insights.map { "- \($0)" }
+        }
+
         appendSection("Ownership", to: &lines) {
             var ownershipLines = [
                 "Registrar: \(report.ownership?.registrar ?? "Unavailable")",
@@ -93,7 +114,8 @@ enum DomainReportExporter {
                 "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))"
+                "DNSSEC: \(dnssecLabel(report.dns.dnssecSigned))",
+                "Patterns: \(report.dns.patternSummary.patterns.joined(separator: " | ").nilIfEmpty ?? "None")"
             ]
             if let provenance = report.sectionProvenance[.dns] {
                 dnsLines.append("Provenance: \(provenanceLabel(provenance))")
@@ -117,6 +139,8 @@ enum DomainReportExporter {
         appendSection("Web", to: &lines) {
             var webLines = [
                 "TLS Status: \(report.web.tlsStatus)",
+                "TLS Grade: \(report.web.tlsGrade.rawValue)",
+                "TLS Highlights: \(report.web.tlsHighlights.joined(separator: " | "))",
                 "Certificate Warning: \(report.web.certificateWarningLevel.title)",
                 "Security Grade: \(report.web.securityGrade ?? "Unavailable")",
                 "HTTP Status: \(report.web.statusCode.map(String.init) ?? "Unavailable")",
@@ -161,6 +185,12 @@ enum DomainReportExporter {
 
         appendSection("Email", to: &lines) {
             var emailLines = [report.email.summary]
+            if let grade = report.email.grade {
+                emailLines.append("Grade: \(grade.rawValue)")
+            }
+            if !report.email.reasons.isEmpty {
+                emailLines.append("Why: \(report.email.reasons.joined(separator: " | "))")
+            }
             emailLines.append("Confidence: \(report.emailConfidence?.title ?? "N/A")")
             if let provenance = report.sectionProvenance[.emailSecurity] {
                 emailLines.append("Provenance: \(provenanceLabel(provenance))")
@@ -217,6 +247,9 @@ enum DomainReportExporter {
                 values.append("None")
                 return values
             }
+            if !report.subdomainGroups.isEmpty {
+                values.append("Groups: \(report.subdomainGroups.map { "\($0.label): \($0.subdomains.count)" }.joined(separator: " | "))")
+            }
             values.append(contentsOf: report.subdomains.map { "- \($0)" })
             return values
         }
@@ -229,9 +262,16 @@ enum DomainReportExporter {
             var values = [
                 "Has Changes: \(changeSummary.hasChanges ? "Yes" : "No")",
                 "Severity: \(changeSummary.severity.title)",
+                "Impact: \(changeSummary.impactClassification.title)",
                 "Inferred Summary: \(changeSummary.message)",
                 "Changed Sections: \(changeSummary.changedSections.isEmpty ? "None" : changeSummary.changedSections.joined(separator: ", "))"
             ]
+            if let riskScoreDelta = changeSummary.riskScoreDelta {
+                values.append("Risk Delta: \(riskScoreDelta >= 0 ? "+" : "")\(riskScoreDelta)")
+            }
+            if !changeSummary.insights.isEmpty {
+                values.append("Insights: \(changeSummary.insights.joined(separator: " | "))")
+            }
             if !changeSummary.observedFacts.isEmpty {
                 values.append("Observed: \(changeSummary.observedFacts.joined(separator: " | "))")
             }
@@ -262,6 +302,11 @@ enum DomainReportExporter {
     }
 
     static func csv(for reports: [DomainReport]) -> String {
+        csv(for: reports, workflowInsights: [])
+    }
+
+    static func csv(for reports: [DomainReport], workflowInsights: [WorkflowInsight]) -> String {
+        let workflowInsightSummary = workflowInsights.map(\.description).joined(separator: " | ")
         let headers = [
             "domain",
             "timestamp",
@@ -269,6 +314,10 @@ enum DomainReportExporter {
             "result_source",
             "resolver",
             "availability",
+            "risk_score",
+            "risk_level",
+            "risk_factors",
+            "insights",
             "availability_confidence",
             "registrar",
             "ownership_confidence",
@@ -277,15 +326,20 @@ enum DomainReportExporter {
             "primary_ip",
             "ptr_record",
             "dnssec_signed",
+            "dns_patterns",
             "tls_status",
+            "tls_grade",
+            "tls_highlights",
             "certificate_warning_level",
             "hsts_preloaded",
             "http_status",
             "http_security_grade",
             "final_url",
             "email_summary",
+            "email_grade",
             "email_confidence",
             "subdomain_count",
+            "subdomain_groups",
             "subdomain_confidence",
             "subdomains",
             "open_ports",
@@ -295,7 +349,9 @@ enum DomainReportExporter {
             "data_sources",
             "audit_note",
             "partial_snapshot",
-            "change_summary"
+            "change_summary",
+            "change_impact",
+            "workflow_insights"
         ]
 
         let rows = reports.map { report in
@@ -307,6 +363,11 @@ enum DomainReportExporter {
             let subdomainCount = String(report.subdomains.count)
             let subdomains = report.subdomains.joined(separator: " | ")
             let openPorts = report.network.openPorts.map(String.init).joined(separator: " | ")
+            let riskFactors = report.riskAssessment.factors.map(\.description).joined(separator: " | ")
+            let insights = report.insights.joined(separator: " | ")
+            let dnsPatterns = report.dns.patternSummary.patterns.joined(separator: " | ")
+            let tlsHighlights = report.web.tlsHighlights.joined(separator: " | ")
+            let subdomainGroups = report.subdomainGroups.map { "\($0.label):\($0.subdomains.count)" }.joined(separator: " | ")
 
             return [
                 report.domain,
@@ -315,6 +376,10 @@ enum DomainReportExporter {
                 report.resultSource.rawValue,
                 report.resolverDisplayName,
                 availabilityLabel(report.availability),
+                "\(report.riskAssessment.score)",
+                report.riskAssessment.level.rawValue,
+                riskFactors,
+                insights,
                 report.availabilityConfidence?.rawValue ?? "",
                 report.ownership?.registrar ?? "",
                 report.ownershipConfidence?.rawValue ?? "",
@@ -323,15 +388,20 @@ enum DomainReportExporter {
                 report.dns.primaryIP ?? "",
                 report.dns.ptrRecord ?? "",
                 dnssecSigned,
+                dnsPatterns,
                 report.web.tlsStatus,
+                report.web.tlsGrade.rawValue,
+                tlsHighlights,
                 report.web.certificateWarningLevel.rawValue,
                 hstsPreloaded,
                 httpStatus,
                 report.web.securityGrade ?? "",
                 report.web.finalURL ?? "",
                 report.email.summary,
+                report.email.grade?.rawValue ?? "",
                 report.emailConfidence?.rawValue ?? "",
                 subdomainCount,
+                subdomainGroups,
                 report.subdomainConfidence?.rawValue ?? "",
                 subdomains,
                 openPorts,
@@ -341,7 +411,9 @@ enum DomainReportExporter {
                 report.dataSources.joined(separator: " | "),
                 report.auditNote ?? "",
                 report.isPartialSnapshot ? "true" : "false",
-                report.changeSummary?.message ?? ""
+                report.changeSummary?.message ?? "",
+                report.changeSummary?.impactClassification.rawValue ?? "",
+                workflowInsightSummary
             ]
         }