krz/domain-dig

an ios app for DNS & SSL analysis

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

e597cdf5b7084634e300af39b622c8552c9cc5e8

unsigned

author: Christian Cleberg <hello@cleberg.net> · 2026-07-25T04:45:56Z

refactor: extract audit surface into DomainViewModel+Audit (v5 step 4, 1/n)

First slice of the v5.0.0 god-file decomposition. DomainViewModel.swift is a
single ~4.9k-line class body; this begins splitting it into focused
`DomainViewModel+<Concern>.swift` extensions, one cohesive concern at a time,
with each move behavior-preserving and verified by build + the test net.

- Moves the audit read/CRUD/export surface (audits/auditSession/auditTimeline,
  updateAuditStatus/Notes, toggleAuditChecklistItem, add/update/remove
  AuditFinding, exportAuditData) into DomainViewModel+Audit.swift as an
  extension. Pure move — no logic changes.
- `startAudit(for:)` intentionally stays on the main type: it drives a live
  inspection to seed the session, so it belongs with the inspection pipeline
  until that is extracted. `persistAuditSessions()` is promoted from private to
  internal so both files can call it (its only cross-file dependency).

No project.pbxproj change is needed — DomainDig/ is a file-system-synchronized
group, so the new file is picked up automatically.

DomainViewModel.swift: 4864 -> 4761 lines. App builds clean; unit suite 58/58.
 DomainDig/DomainViewModel+Audit.swift | 114 ++++++++++++++++++++++++++++++++++
 DomainDig/DomainViewModel.swift       | 105 +------------------------------
 2 files changed, 115 insertions(+), 104 deletions(-)

diff --git a/DomainDig/DomainViewModel+Audit.swift b/DomainDig/DomainViewModel+Audit.swift
new file mode 100644
index 0000000..a825b60
--- /dev/null
+++ b/DomainDig/DomainViewModel+Audit.swift
@@ -0,0 +1,114 @@
+import Foundation
+import SwiftUI
+
+/// Audit review surface of `DomainViewModel`: reading audit sessions and their
+/// timeline, and mutating a session's status, notes, checklist, and findings.
+///
+/// `startAudit(for:)` deliberately stays on the main type — it drives a live
+/// inspection to seed the session, so it belongs with the inspection pipeline
+/// until that is extracted. Everything here operates purely on `auditSessions`
+/// and persists through `persistAuditSessions()`.
+extension DomainViewModel {
+    func audits(for domain: String) -> [AuditSession] {
+        auditSessions
+            .filter { $0.domain.caseInsensitiveCompare(domain) == .orderedSame }
+            .sorted { $0.createdAt > $1.createdAt }
+    }
+
+    func auditSession(withID id: UUID) -> AuditSession? {
+        auditSessions.first(where: { $0.id == id })
+    }
+
+    func auditTimeline(for domain: String) -> [AuditTimelinePoint] {
+        let sessions = audits(for: domain).sorted { $0.createdAt > $1.createdAt }
+        return sessions.map { session in
+            let repeatedIssues = sessions
+                .filter { $0.id != session.id }
+                .flatMap(\.findings)
+                .map { $0.title.lowercased() }
+            let repeatedIssueCount = session.findings.filter {
+                repeatedIssues.contains($0.title.lowercased())
+            }.count
+
+            return AuditTimelinePoint(
+                id: session.id,
+                sessionID: session.id,
+                domain: session.domain,
+                createdAt: session.createdAt,
+                status: session.status,
+                findingCount: session.findings.count,
+                openHighSeverityCount: session.findings.filter { $0.severity == .high && $0.status != .resolved }.count,
+                repeatedIssueCount: repeatedIssueCount
+            )
+        }
+    }
+
+    func updateAuditStatus(_ status: AuditStatus, sessionID: UUID) {
+        guard let index = auditSessions.firstIndex(where: { $0.id == sessionID }) else { return }
+        auditSessions[index].status = status
+        persistAuditSessions()
+    }
+
+    func updateAuditNotes(_ notes: String, sessionID: UUID) {
+        guard let index = auditSessions.firstIndex(where: { $0.id == sessionID }) else { return }
+        auditSessions[index].notes = notes.trimmingCharacters(in: .whitespacesAndNewlines)
+        persistAuditSessions()
+    }
+
+    func toggleAuditChecklistItem(sessionID: UUID, itemID: UUID) {
+        guard let sessionIndex = auditSessions.firstIndex(where: { $0.id == sessionID }),
+              let itemIndex = auditSessions[sessionIndex].checklist.firstIndex(where: { $0.id == itemID }) else {
+            return
+        }
+
+        auditSessions[sessionIndex].checklist[itemIndex].isComplete.toggle()
+        auditSessions[sessionIndex].checklist[itemIndex].completedAt = auditSessions[sessionIndex].checklist[itemIndex].isComplete ? Date() : nil
+        persistAuditSessions()
+    }
+
+    func addAuditFinding(
+        sessionID: UUID,
+        title: String,
+        severity: AuditFindingSeverity,
+        summary: String,
+        evidenceReferences: [String],
+        notes: String,
+        checklistAreas: [AuditChecklistArea]
+    ) {
+        guard let index = auditSessions.firstIndex(where: { $0.id == sessionID }) else { return }
+        let finding = AuditFinding(
+            title: title,
+            severity: severity,
+            summary: summary,
+            evidenceReferences: evidenceReferences,
+            notes: notes,
+            status: .open,
+            checklistAreas: checklistAreas
+        )
+        auditSessions[index].findings.insert(finding, at: 0)
+        persistAuditSessions()
+    }
+
+    func updateAuditFinding(_ finding: AuditFinding, sessionID: UUID) {
+        guard let sessionIndex = auditSessions.firstIndex(where: { $0.id == sessionID }),
+              let findingIndex = auditSessions[sessionIndex].findings.firstIndex(where: { $0.id == finding.id }) else {
+            return
+        }
+
+        var updatedFinding = finding
+        updatedFinding.updatedAt = Date()
+        auditSessions[sessionIndex].findings[findingIndex] = updatedFinding
+        persistAuditSessions()
+    }
+
+    func removeAuditFindings(at offsets: IndexSet, sessionID: UUID) {
+        guard let sessionIndex = auditSessions.firstIndex(where: { $0.id == sessionID }) else { return }
+        auditSessions[sessionIndex].findings.remove(atOffsets: offsets)
+        persistAuditSessions()
+    }
+
+    func exportAuditData(sessionID: UUID, format: AuditExportFormat) -> Data? {
+        guard let session = auditSession(withID: sessionID) else { return nil }
+        return try? AuditExporter.data(for: session, format: format)
+    }
+}
diff --git a/DomainDig/DomainViewModel.swift b/DomainDig/DomainViewModel.swift
index b5f51c3..fdf6b84 100644
--- a/DomainDig/DomainViewModel.swift
+++ b/DomainDig/DomainViewModel.swift
@@ -1546,40 +1546,6 @@ final class DomainViewModel {
         return try? DomainReportExporter.data(for: currentReport, format: format)
     }
 
-    func audits(for domain: String) -> [AuditSession] {
-        auditSessions
-            .filter { $0.domain.caseInsensitiveCompare(domain) == .orderedSame }
-            .sorted { $0.createdAt > $1.createdAt }
-    }
-
-    func auditSession(withID id: UUID) -> AuditSession? {
-        auditSessions.first(where: { $0.id == id })
-    }
-
-    func auditTimeline(for domain: String) -> [AuditTimelinePoint] {
-        let sessions = audits(for: domain).sorted { $0.createdAt > $1.createdAt }
-        return sessions.map { session in
-            let repeatedIssues = sessions
-                .filter { $0.id != session.id }
-                .flatMap(\.findings)
-                .map { $0.title.lowercased() }
-            let repeatedIssueCount = session.findings.filter {
-                repeatedIssues.contains($0.title.lowercased())
-            }.count
-
-            return AuditTimelinePoint(
-                id: session.id,
-                sessionID: session.id,
-                domain: session.domain,
-                createdAt: session.createdAt,
-                status: session.status,
-                findingCount: session.findings.count,
-                openHighSeverityCount: session.findings.filter { $0.severity == .high && $0.status != .resolved }.count,
-                repeatedIssueCount: repeatedIssueCount
-            )
-        }
-    }
-
     @discardableResult
     func startAudit(for domain: String, reviewer: String? = nil) async -> AuditSession? {
         let normalizedDomain = domain
@@ -1625,75 +1591,6 @@ final class DomainViewModel {
         return session
     }
 
-    func updateAuditStatus(_ status: AuditStatus, sessionID: UUID) {
-        guard let index = auditSessions.firstIndex(where: { $0.id == sessionID }) else { return }
-        auditSessions[index].status = status
-        persistAuditSessions()
-    }
-
-    func updateAuditNotes(_ notes: String, sessionID: UUID) {
-        guard let index = auditSessions.firstIndex(where: { $0.id == sessionID }) else { return }
-        auditSessions[index].notes = notes.trimmingCharacters(in: .whitespacesAndNewlines)
-        persistAuditSessions()
-    }
-
-    func toggleAuditChecklistItem(sessionID: UUID, itemID: UUID) {
-        guard let sessionIndex = auditSessions.firstIndex(where: { $0.id == sessionID }),
-              let itemIndex = auditSessions[sessionIndex].checklist.firstIndex(where: { $0.id == itemID }) else {
-            return
-        }
-
-        auditSessions[sessionIndex].checklist[itemIndex].isComplete.toggle()
-        auditSessions[sessionIndex].checklist[itemIndex].completedAt = auditSessions[sessionIndex].checklist[itemIndex].isComplete ? Date() : nil
-        persistAuditSessions()
-    }
-
-    func addAuditFinding(
-        sessionID: UUID,
-        title: String,
-        severity: AuditFindingSeverity,
-        summary: String,
-        evidenceReferences: [String],
-        notes: String,
-        checklistAreas: [AuditChecklistArea]
-    ) {
-        guard let index = auditSessions.firstIndex(where: { $0.id == sessionID }) else { return }
-        let finding = AuditFinding(
-            title: title,
-            severity: severity,
-            summary: summary,
-            evidenceReferences: evidenceReferences,
-            notes: notes,
-            status: .open,
-            checklistAreas: checklistAreas
-        )
-        auditSessions[index].findings.insert(finding, at: 0)
-        persistAuditSessions()
-    }
-
-    func updateAuditFinding(_ finding: AuditFinding, sessionID: UUID) {
-        guard let sessionIndex = auditSessions.firstIndex(where: { $0.id == sessionID }),
-              let findingIndex = auditSessions[sessionIndex].findings.firstIndex(where: { $0.id == finding.id }) else {
-            return
-        }
-
-        var updatedFinding = finding
-        updatedFinding.updatedAt = Date()
-        auditSessions[sessionIndex].findings[findingIndex] = updatedFinding
-        persistAuditSessions()
-    }
-
-    func removeAuditFindings(at offsets: IndexSet, sessionID: UUID) {
-        guard let sessionIndex = auditSessions.firstIndex(where: { $0.id == sessionID }) else { return }
-        auditSessions[sessionIndex].findings.remove(atOffsets: offsets)
-        persistAuditSessions()
-    }
-
-    func exportAuditData(sessionID: UUID, format: AuditExportFormat) -> Data? {
-        guard let session = auditSession(withID: sessionID) else { return nil }
-        return try? AuditExporter.data(for: session, format: format)
-    }
-
     func loadOwnershipHistory() async {
         guard !searchedDomain.isEmpty else { return }
         guard DataAccessService.hasAccess(to: .ownershipHistory) else {
@@ -2605,7 +2502,7 @@ final class DomainViewModel {
         DomainDebugLog.signpostEnd("DomainViewModel.persistHistory", start: persistStartedAt, extra: "count=\(history.count)")
     }
 
-    private func persistAuditSessions() {
+    func persistAuditSessions() {
         DomainDataPortabilityService.saveAuditSessions(auditSessions)
         refreshDataLifecycleSummary()
     }