diff options
| author | Christian Cleberg <[email protected]> | 2026-07-24 23:45:56 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-25 00:00:29 -0500 |
| commit | e597cdf5b7084634e300af39b622c8552c9cc5e8 (patch) | |
| tree | 9c02254bbd50bf0772076857be69f50254dfe7b3 | |
| parent | 99e4623af1b08f36120a01b67cbe60df99668651 (diff) | |
| download | domain-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.
| -rw-r--r-- | DomainDig/DomainViewModel+Audit.swift | 114 | ||||
| -rw-r--r-- | 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() } |
