summaryrefslogtreecommitdiff
path: root/DomainDig
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-24 23:45:56 -0500
committerChristian Cleberg <[email protected]>2026-07-25 00:00:29 -0500
commite597cdf5b7084634e300af39b622c8552c9cc5e8 (patch)
tree9c02254bbd50bf0772076857be69f50254dfe7b3 /DomainDig
parent99e4623af1b08f36120a01b67cbe60df99668651 (diff)
downloaddomain-dig-e597cdf5b7084634e300af39b622c8552c9cc5e8.tar.gz
domain-dig-e597cdf5b7084634e300af39b622c8552c9cc5e8.tar.bz2
domain-dig-e597cdf5b7084634e300af39b622c8552c9cc5e8.zip
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.
Diffstat (limited to 'DomainDig')
-rw-r--r--DomainDig/DomainViewModel+Audit.swift114
-rw-r--r--DomainDig/DomainViewModel.swift105
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()
}