Commit 017785c4ce

017785c4ce07cfe026a56d72963eb977fea06440

parent: dad5253250

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-22 07:24 UTC

Build Inspection, Provenance and Failures straight from the snapshot

Grouping the parameters left both call sites spelling out the same 46 lines of
snapshot.x -> x copying, which is duplication SonarCloud was right to flag: the
two lists could drift apart and nothing would say so.

Each of those three groups is populated entirely from the snapshot, so they get
an init(snapshot:) and the call sites collapse to .init(snapshot: snapshot).
The other four groups genuinely differ between the two sites and stay explicit.

Declared in extensions so the structs keep their synthesized memberwise
initializers, which the tests and the remaining call sites use.

Layout: unified · split

DomainDig/DomainMonitoringService.swift +3 −52
@@ -526,24 +526,7 @@ final class DomainMonitoringService {
526 trackedDomainID: trackedDomainID, 526 trackedDomainID: trackedDomainID,
527 note: trackedDomains.first(where: { $0.id == trackedDomainID })?.note 527 note: trackedDomains.first(where: { $0.id == trackedDomainID })?.note
528 ), 528 ),
529 inspection: HistoryEntry.Inspection( 529 inspection: .init(snapshot: snapshot),
530 dnsSections: snapshot.dnsSections,
531 sslInfo: snapshot.sslInfo,
532 httpHeaders: snapshot.httpHeaders,
533 reachabilityResults: snapshot.reachabilityResults,
534 ipGeolocation: snapshot.ipGeolocation,
535 emailSecurity: snapshot.emailSecurity,
536 mtaSts: snapshot.emailSecurity?.mtaSts,
537 ptrRecord: snapshot.ptrRecord,
538 redirectChain: snapshot.redirectChain,
539 subdomains: snapshot.subdomains,
540 extendedSubdomains: snapshot.extendedSubdomains,
541 dnsHistory: snapshot.dnsHistory,
542 portScanResults: snapshot.portScanResults,
543 hstsPreloaded: snapshot.hstsPreloaded,
544 availabilityResult: snapshot.availabilityResult,
545 suggestions: snapshot.suggestions
546 ),
547 registration: HistoryEntry.Registration( 530 registration: HistoryEntry.Registration(
548 ownership: snapshot.ownership, 531 ownership: snapshot.ownership,
549 ownershipHistory: snapshot.ownershipHistory, 532 ownershipHistory: snapshot.ownershipHistory,
@@ -552,22 +535,7 @@ final class DomainMonitoringService {
552 intelligence: HistoryEntry.Intelligence( 535 intelligence: HistoryEntry.Intelligence(
553 reputation: snapshot.reputation 536 reputation: snapshot.reputation
554 ), 537 ),
555 provenance: HistoryEntry.Provenance( 538 provenance: .init(snapshot: snapshot),
556 appVersion: snapshot.appVersion,
557 resultSource: snapshot.resultSource,
558 dataSources: snapshot.dataSources,
559 provenanceBySection: snapshot.provenanceBySection,
560 availabilityConfidence: snapshot.availabilityConfidence,
561 ownershipConfidence: snapshot.ownershipConfidence,
562 subdomainConfidence: snapshot.subdomainConfidence,
563 emailSecurityConfidence: snapshot.emailSecurityConfidence,
564 geolocationConfidence: snapshot.geolocationConfidence,
565 isPartialSnapshot: snapshot.isPartialSnapshot,
566 validationIssues: snapshot.validationIssues,
567 resolverDisplayName: snapshot.resolverDisplayName,
568 resolverURLString: snapshot.resolverURLString,
569 totalLookupDurationMs: snapshot.totalLookupDurationMs
570 ),
571 summary: HistoryEntry.Summary( 539 summary: HistoryEntry.Summary(
572 primaryIP: Self.primaryIPAddress(from: snapshot), 540 primaryIP: Self.primaryIPAddress(from: snapshot),
573 finalRedirectURL: snapshot.redirectChain.last?.url, 541 finalRedirectURL: snapshot.redirectChain.last?.url,
@@ -576,24 +544,7 @@ final class DomainMonitoringService {
576 httpGradeSummary: snapshot.httpSecurityGrade ?? snapshot.httpHeadersError, 544 httpGradeSummary: snapshot.httpSecurityGrade ?? snapshot.httpHeadersError,
577 changeSummary: changeSummary 545 changeSummary: changeSummary
578 ), 546 ),
579 failures: HistoryEntry.Failures( 547 failures: .init(snapshot: snapshot)
580 errorDetails: snapshot.errorDetails,
581 sslError: snapshot.sslError,
582 httpHeadersError: snapshot.httpHeadersError,
583 reachabilityError: snapshot.reachabilityError,
584 ipGeolocationError: snapshot.ipGeolocationError,
585 emailSecurityError: snapshot.emailSecurityError,
586 ownershipError: snapshot.ownershipError,
587 ownershipHistoryError: snapshot.ownershipHistoryError,
588 ptrError: snapshot.ptrError,
589 redirectChainError: snapshot.redirectChainError,
590 subdomainsError: snapshot.subdomainsError,
591 extendedSubdomainsError: snapshot.extendedSubdomainsError,
592 dnsHistoryError: snapshot.dnsHistoryError,
593 domainPricingError: snapshot.domainPricingError,
594 reputationError: snapshot.reputationError,
595 portScanError: snapshot.portScanError
596 )
597 ) 548 )
598 549
599 history.insert(entry, at: 0) 550 history.insert(entry, at: 0)
DomainDig/DomainViewModel.swift +3 −52
@@ -1942,24 +1942,7 @@ final class DomainViewModel {
1942 trackedDomainID: trackedDomainID, 1942 trackedDomainID: trackedDomainID,
1943 note: currentHistoryEntry?.note 1943 note: currentHistoryEntry?.note
1944 ), 1944 ),
1945 inspection: HistoryEntry.Inspection( 1945 inspection: .init(snapshot: snapshot),
1946 dnsSections: snapshot.dnsSections,
1947 sslInfo: snapshot.sslInfo,
1948 httpHeaders: snapshot.httpHeaders,
1949 reachabilityResults: snapshot.reachabilityResults,
1950 ipGeolocation: snapshot.ipGeolocation,
1951 emailSecurity: snapshot.emailSecurity,
1952 mtaSts: snapshot.emailSecurity?.mtaSts,
1953 ptrRecord: snapshot.ptrRecord,
1954 redirectChain: snapshot.redirectChain,
1955 subdomains: snapshot.subdomains,
1956 extendedSubdomains: snapshot.extendedSubdomains,
1957 dnsHistory: snapshot.dnsHistory,
1958 portScanResults: snapshot.portScanResults,
1959 hstsPreloaded: snapshot.hstsPreloaded,
1960 availabilityResult: snapshot.availabilityResult,
1961 suggestions: snapshot.suggestions
1962 ),
1963 registration: HistoryEntry.Registration( 1946 registration: HistoryEntry.Registration(
1964 ownership: snapshot.ownership, 1947 ownership: snapshot.ownership,
1965 ownershipHistory: snapshot.ownershipHistory, 1948 ownershipHistory: snapshot.ownershipHistory,
@@ -1976,22 +1959,7 @@ final class DomainViewModel {
1976 intelligenceTimeline: intelligence.timelineEvents, 1959 intelligenceTimeline: intelligence.timelineEvents,
1977 reputation: snapshot.reputation 1960 reputation: snapshot.reputation
1978 ), 1961 ),
1979 provenance: HistoryEntry.Provenance( 1962 provenance: .init(snapshot: snapshot),
1980 appVersion: snapshot.appVersion,
1981 resultSource: snapshot.resultSource,
1982 dataSources: snapshot.dataSources,
1983 provenanceBySection: snapshot.provenanceBySection,
1984 availabilityConfidence: snapshot.availabilityConfidence,
1985 ownershipConfidence: snapshot.ownershipConfidence,
1986 subdomainConfidence: snapshot.subdomainConfidence,
1987 emailSecurityConfidence: snapshot.emailSecurityConfidence,
1988 geolocationConfidence: snapshot.geolocationConfidence,
1989 isPartialSnapshot: snapshot.isPartialSnapshot,
1990 validationIssues: snapshot.validationIssues,
1991 resolverDisplayName: snapshot.resolverDisplayName,
1992 resolverURLString: snapshot.resolverURLString,
1993 totalLookupDurationMs: snapshot.totalLookupDurationMs
1994 ),
1995 summary: HistoryEntry.Summary( 1963 summary: HistoryEntry.Summary(
1996 primaryIP: Self.primaryIPAddress(from: snapshot), 1964 primaryIP: Self.primaryIPAddress(from: snapshot),
1997 finalRedirectURL: Self.finalRedirectTarget(from: snapshot), 1965 finalRedirectURL: Self.finalRedirectTarget(from: snapshot),
@@ -2004,24 +1972,7 @@ final class DomainViewModel {
2004 changeCount: domainDiff?.changeCount ?? changeSummary?.changedSections.count ?? 0, 1972 changeCount: domainDiff?.changeCount ?? changeSummary?.changedSections.count ?? 0,
2005 severitySummary: changeSummary?.severity 1973 severitySummary: changeSummary?.severity
2006 ), 1974 ),
2007 failures: HistoryEntry.Failures( 1975 failures: .init(snapshot: snapshot)
2008 errorDetails: snapshot.errorDetails,
2009 sslError: snapshot.sslError,
2010 httpHeadersError: snapshot.httpHeadersError,
2011 reachabilityError: snapshot.reachabilityError,
2012 ipGeolocationError: snapshot.ipGeolocationError,
2013 emailSecurityError: snapshot.emailSecurityError,
2014 ownershipError: snapshot.ownershipError,
2015 ownershipHistoryError: snapshot.ownershipHistoryError,
2016 ptrError: snapshot.ptrError,
2017 redirectChainError: snapshot.redirectChainError,
2018 subdomainsError: snapshot.subdomainsError,
2019 extendedSubdomainsError: snapshot.extendedSubdomainsError,
2020 dnsHistoryError: snapshot.dnsHistoryError,
2021 domainPricingError: snapshot.domainPricingError,
2022 reputationError: snapshot.reputationError,
2023 portScanError: snapshot.portScanError
2024 )
2025 ) 1976 )
2026 1977
2027 if updateCurrentState { 1978 if updateCurrentState {
DomainDig/Models.swift +83
@@ -1267,6 +1267,86 @@ struct MonitoringPendingAlert: Codable, Identifiable, Equatable, Sendable {
1267 } 1267 }
1268} 1268}
1269 1269
1270
1271// Declared in an extension so the struct keeps its synthesized memberwise
1272// initializer: adding one inside the body would suppress it.
1273extension HistoryEntry.Inspection {
1274 /// Every field here is copied straight off the snapshot, and both call
1275 /// sites did exactly that. Naming it once keeps them from drifting.
1276 init(snapshot: LookupSnapshot) {
1277 self.init(
1278 dnsSections: snapshot.dnsSections,
1279 sslInfo: snapshot.sslInfo,
1280 httpHeaders: snapshot.httpHeaders,
1281 reachabilityResults: snapshot.reachabilityResults,
1282 ipGeolocation: snapshot.ipGeolocation,
1283 emailSecurity: snapshot.emailSecurity,
1284 mtaSts: snapshot.emailSecurity?.mtaSts,
1285 ptrRecord: snapshot.ptrRecord,
1286 redirectChain: snapshot.redirectChain,
1287 subdomains: snapshot.subdomains,
1288 extendedSubdomains: snapshot.extendedSubdomains,
1289 dnsHistory: snapshot.dnsHistory,
1290 portScanResults: snapshot.portScanResults,
1291 hstsPreloaded: snapshot.hstsPreloaded,
1292 availabilityResult: snapshot.availabilityResult,
1293 suggestions: snapshot.suggestions
1294 )
1295 }
1296}
1297
1298// Declared in an extension so the struct keeps its synthesized memberwise
1299// initializer: adding one inside the body would suppress it.
1300extension HistoryEntry.Provenance {
1301 /// Every field here is copied straight off the snapshot, and both call
1302 /// sites did exactly that. Naming it once keeps them from drifting.
1303 init(snapshot: LookupSnapshot) {
1304 self.init(
1305 appVersion: snapshot.appVersion,
1306 resultSource: snapshot.resultSource,
1307 dataSources: snapshot.dataSources,
1308 provenanceBySection: snapshot.provenanceBySection,
1309 availabilityConfidence: snapshot.availabilityConfidence,
1310 ownershipConfidence: snapshot.ownershipConfidence,
1311 subdomainConfidence: snapshot.subdomainConfidence,
1312 emailSecurityConfidence: snapshot.emailSecurityConfidence,
1313 geolocationConfidence: snapshot.geolocationConfidence,
1314 isPartialSnapshot: snapshot.isPartialSnapshot,
1315 validationIssues: snapshot.validationIssues,
1316 resolverDisplayName: snapshot.resolverDisplayName,
1317 resolverURLString: snapshot.resolverURLString,
1318 totalLookupDurationMs: snapshot.totalLookupDurationMs
1319 )
1320 }
1321}
1322
1323// Declared in an extension so the struct keeps its synthesized memberwise
1324// initializer: adding one inside the body would suppress it.
1325extension HistoryEntry.Failures {
1326 /// Every field here is copied straight off the snapshot, and both call
1327 /// sites did exactly that. Naming it once keeps them from drifting.
1328 init(snapshot: LookupSnapshot) {
1329 self.init(
1330 errorDetails: snapshot.errorDetails,
1331 sslError: snapshot.sslError,
1332 httpHeadersError: snapshot.httpHeadersError,
1333 reachabilityError: snapshot.reachabilityError,
1334 ipGeolocationError: snapshot.ipGeolocationError,
1335 emailSecurityError: snapshot.emailSecurityError,
1336 ownershipError: snapshot.ownershipError,
1337 ownershipHistoryError: snapshot.ownershipHistoryError,
1338 ptrError: snapshot.ptrError,
1339 redirectChainError: snapshot.redirectChainError,
1340 subdomainsError: snapshot.subdomainsError,
1341 extendedSubdomainsError: snapshot.extendedSubdomainsError,
1342 dnsHistoryError: snapshot.dnsHistoryError,
1343 domainPricingError: snapshot.domainPricingError,
1344 reputationError: snapshot.reputationError,
1345 portScanError: snapshot.portScanError
1346 )
1347 }
1348}
1349
1270struct TrackedDomain: Codable, Identifiable, Equatable { 1350struct TrackedDomain: Codable, Identifiable, Equatable {
1271 let id: UUID 1351 let id: UUID
1272 var domain: String 1352 var domain: String
@@ -2436,6 +2516,7 @@ struct HistoryEntry: Identifiable, Codable {
2436 var hstsPreloaded: Bool? = nil 2516 var hstsPreloaded: Bool? = nil
2437 var availabilityResult: DomainAvailabilityResult? = nil 2517 var availabilityResult: DomainAvailabilityResult? = nil
2438 var suggestions: [DomainSuggestionResult] = [] 2518 var suggestions: [DomainSuggestionResult] = []
2519
2439 } 2520 }
2440 2521
2441 /// Who owns the domain and where it is hosted. 2522 /// Who owns the domain and where it is hosted.
@@ -2474,6 +2555,7 @@ struct HistoryEntry: Identifiable, Codable {
2474 var resolverDisplayName: String 2555 var resolverDisplayName: String
2475 var resolverURLString: String 2556 var resolverURLString: String
2476 var totalLookupDurationMs: Int? = nil 2557 var totalLookupDurationMs: Int? = nil
2558
2477 } 2559 }
2478 2560
2479 /// Precomputed display values and change tracking. 2561 /// Precomputed display values and change tracking.
@@ -2508,6 +2590,7 @@ struct HistoryEntry: Identifiable, Codable {
2508 var domainPricingError: String? = nil 2590 var domainPricingError: String? = nil
2509 var reputationError: String? = nil 2591 var reputationError: String? = nil
2510 var portScanError: String? = nil 2592 var portScanError: String? = nil
2593
2511 } 2594 }
2512 2595
2513 init(identity: Identity, 2596 init(identity: Identity,