1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
|
# DomainDig Release Roadmap
Priority lens: **new user-facing features.** The inspection engine is already
deep (DNS, DNSSEC, CAA, TLS, TLSA/DANE, email security incl. BIMI/MTA-STS, RDAP,
ports, geolocation, subdomains, availability). The next several releases invest
in *reach and surfacing* — getting that data onto more iOS surfaces and into more
workflows — rather than adding raw protocol checks.
Current version: `v4.9.0`.
## v4.4.1 Patch: Release Readiness — ✅ shipped
- Consolidated Audit Mode onto the single `DomainDig/DomainDig/Audit*`
implementation and retired the prototype files.
- Aligned `AppVersion.current`, Xcode marketing version, and build number.
- Included audit sessions in backup/restore counts, summaries, and merge behavior.
- Removed the retired `DomainDigCLI` target and refreshed README/architecture docs.
## v4.5.0 Minor: Home Screen & Shortcuts Reach — ✅ shipped
Goal: put DomainDig data and actions where the user already is.
- **App Intents / Shortcuts** — `InspectDomainIntent`, `AddToWatchlistIntent`, and
`RunSweepIntent`, exposed via `DomainDigShortcuts` for Shortcuts, Spotlight, the
Action button, and Siri.
- **`domaindig://` deep links** — `inspect`, `watch`, `domain` (detail), and
`sweep`, routed in `RootTabView`.
- **WidgetKit portfolio widget** (Home Screen small/medium/large) — per-domain
health, certificate countdowns, and portfolio health counts, shared from the app
via an App Group; tapping a domain deep-links into its detail.
Deferred to a later minor: **Lock Screen accessory widget families** and a richer
per-widget "last change" indicator.
## v4.6.0 Minor: Alerts, Glances & iPad — ✅ shipped
Goal: make monitoring and results feel first-class across contexts.
- **Sweep Live Activity** — a batch/watchlist sweep drives a Live Activity with a
progress bar, current domain, and change/warning counts on the Lock Screen and
in the Dynamic Island (`SweepActivityController` around the batch pipeline).
- **Share extension** (`DomainDigShareExtension`) — "Dig Domain" accepts a web URL
from the system share sheet, extracts the host, and hands it to the app via the
App Group inbox; the app inspects it on next activation.
- **iPad-optimized layout** — `RootTabView` renders a `NavigationSplitView`
(sidebar + detail) in the regular size class and the tab bar in compact.
- **Actionable notifications** — per-domain `threadIdentifier` grouping, a
"Re-inspect" action, and taps that route into the domain's detail.
Deferred: monitoring-alert Live Activities (only the sweep activity shipped) and
Lock Screen accessory widget families (carried over from v4.5.0).
## v4.7.0 Minor: Intelligence & Comparison — ✅ shipped
Goal: help users interpret and organize, not just collect.
- **Domain-vs-domain comparison** — `DiffService.compare(domainA:domainB:)`
reuses the existing section-diff builders; `DomainCompareView` (Watchlist
toolbar → "Compare Domains") picks two tracked domains and renders the result
with the existing diff section UI.
- **Reputation / blocklist signals** — a new pluggable data source
(`ExternalDataService.reputation(domain:)`, Pro+) mirroring the existing
ownership/DNS-history/pricing enrichment pattern. Ships with no bundled
third-party endpoint; folds a listed status into risk score/factors and
insights, so it rides the existing report and monitoring change-severity
pipeline rather than needing bespoke monitoring wiring.
- **Tags and saved views** for the watchlist — freeform tags per tracked
domain, tag filter chips, and named saved filter/sort/tag presets
(UserDefaults-backed; not yet part of backup/restore).
## v4.8.0 Minor: Reporting & Sharing — ✅ shipped
Goal: turn point-in-time snapshots into shareable, scheduled deliverables.
- **Markdown and PDF export formats** — `DomainExportFormat` gains `.markdown`
and `.pdf` alongside text/csv/json. Markdown reuses the existing text-export
content via a line-based transform (never drifts from the text export); PDF
renders that Markdown via `UIGraphicsPDFRenderer`, mirroring the approach
`AuditExporter` already used for audit sessions.
- **Scheduled report generation** — `ScheduledReportService` /
`ScheduledReportScheduler` (Settings → Scheduled Reports): a BGTaskScheduler-
driven daily/weekly job that builds a markdown/PDF/JSON report bundle for all
tracked domains, writes it locally, logs the run, and notifies when ready.
Mirrors `DomainMonitoringService`'s headless, storage-backed design; gated
behind the same Pro `.automatedMonitoring` capability.
- **Stronger share affordances** — "Export Markdown"/"Export PDF" added to the
single-result, batch, watchlist, and workflow export menus; generated
scheduled reports are individually shareable from their log.
- **Export consistency verified** — the local API already serves the canonical
`DomainReport` directly (no field allowlist), so `reputation`, `domainPricing`,
and every other field added since v4.7.0 already flow through automatically.
No code change was needed there.
Deferred/scoped out: scheduled-report settings and logs are UserDefaults-only
(not part of `DomainDataPortabilityService` backup/restore), same reasoning as
v4.7.0's watchlist saved views — this is local automation config, not
user-authored content.
## v4.8.1 Patch: Reporting & Sharing Fixes — ✅ shipped
Goal: fix what UAT of v4.8.0 turned up.
- **Scheduled reports were unreachable manually** — the Overview section wrapped
every control in a single `VStack` inside one `List` row, so SwiftUI collapsed
them into one tap target and the Cadence `Picker` captured taps meant for
"Generate Now". Each control is now its own row.
- **Pro gate completed on that screen** — `.automatedMonitoring` previously
disabled only the toggle, leaving both pickers and "Generate Now" interactive
on Free where they silently no-opped against the service-side guard.
- **Markdown/PDF reports rendered `=` underlines as bullets** — the plain-text
transform only recognized `-`, so `batchText`'s title underline and its
48-character inter-report separators leaked through as literal list items.
- **Duplicate DNS record values** — the report concatenated apex and wildcard
records without dedup, listing every value twice on domains with wildcard DNS.
- **Inspect tab keyboard behavior** — removed the "Dismiss Keyboard" toolbar
button and the launch-time focus that raised the keyboard on app open.
- **In-app purchases were unbuyable** — none of the four product ID constants in
`PurchaseService` matched the auto-renewable subscriptions configured in App
Store Connect, so `Product.products(for:)` returned nothing and `tier(for:)`
resolved every purchase to `.free`. Product IDs are permanent once created, so
the constants were corrected to match the store rather than the reverse.
- **Local StoreKit testing** — added `DomainDig.storekit` mirroring the App Store
Connect group (Pro+ at level 1, Pro at level 2) and wired it into the Run
action, so the purchase and entitlement paths can be exercised without the
`DOMAIN_DIG_FORCE_PRO_PLUS` launch argument that bypasses StoreKit entirely.
Follow-ups filed during UAT (#8, #9, #10) were all resolved in v4.8.2.
## v4.8.2 Patch: Delivery Visibility & Build Health — ✅ shipped
Goal: close the UAT follow-ups and make failures legible instead of silent.
- **Disabled integrations no longer swallow events** (#8) — `enqueue(events:)`
filtered to enabled targets before writing any `DeliveryRecord`, so events
routed to a disabled integration vanished entirely. They now log a `.skipped`
entry with a reason. `sendTest` also respects `isEnabled`, which previously
delivered against targets that dropped every real event.
- **"Process Queue Now" forces backed-off retries** (#9) — it only restarted the
processing task, never moving `nextAttemptAt`, so an item in backoff (up to an
hour) stayed undue and the button appeared inert. It now pulls queued items
forward, and reports an empty queue instead of doing nothing silently.
- **Unreachable domains report as unreachable** (#10) — when the snapshot
fallback fired, the run compared old data against itself and claimed "No
meaningful changes" for a domain it never reached. `MonitoringDomainResult`
now carries `unreachableReason`, the summary says so, and a warning-severity
`monitoringFailure` reaches configured integrations.
- **Swift 6 concurrency warnings cleared** — `SweepActivityAttributes` is
explicitly `nonisolated` (the app target sets
`SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor` while the widget target does not),
and `LocalAPIService`'s logger closures capture `self` coherently. Build is
warning-free.
- **StoreKit configuration corrected and synced** — the scheme's path was wrong,
and the hand-authored file has been replaced by `SyncedProducts.storekit`,
synced against App Store Connect. Registered in the project without target
membership so it is not bundled into shipping builds.
## v4.8.3 Patch: Static Analysis Cleanup — ✅ shipped
Goal: clear the SonarCloud new-code backlog without changing behavior.
- **Dead confidence conditionals fixed** (4 bugs) —
`DomainInspectionService`'s `confidenceFor*` helpers each returned
`error == nil ? .low : .low`. The conditional was inert, so the unused `error`
parameter was dropped alongside it.
- **Identical switch branches merged** — 14 sites in `DomainViewModel` handled
`.empty(message)` and `.error(message)` with byte-identical bodies; they now
share one `case let .empty(message), let .error(message):`.
- **Duplicate implementations consolidated** — `clearPresentedResults()` now
delegates to `reset()`, `String.nonEmpty` was folded into `nilIfEmpty`, and
`ExportFormat.id` derives from `fileExtension`.
- **Nested ternaries extracted** — grade-to-tone and impact-to-color mappings
became `TLSGrade.tone`, `EmailSecurityGrade.tone`, and
`ChangeImpactClassification.color`, replacing `ContentView`'s private
`impactColor` and the duplicate mapping in `BatchResultsView`.
- **Remaining smells** — empty closures and singleton inits documented, unused
protocol-conformance parameters marked `_`, `CloudSyncTrigger.import` renamed
to `imported` (raw value preserved), `_serverTrust`/`_tlsMetadata` renamed,
nested `if`s merged in the DER parser, and deep closure nesting flattened in
`PortScanService` and `IntegrationService`.
Left open deliberately: `swift:S107` (initializer parameter counts on model
memberwise inits), `swift:S115` (constants mirroring DoH/ipapi JSON keys),
`swift:S1075` (false positives on `https://` literals), and two `swift:S117`
hits on SwiftUI `$binding` shorthand in `AuditModeView`, which cannot be
renamed. These want a *Won't Fix* / *Safe* resolution in SonarCloud rather than
a code change.
## v4.9.0 Minor: Accessibility, Appearance & Engineering Health — ✅ shipped
Goal: make the app usable by every iOS user — full accessibility pass (#21),
light mode, and the engineering scaffolding to keep both from regressing.
- **Semantic colour system** — every hard-coded colour replaced with adaptive
colorsets in `Shared/Colors.xcassets` (Any/Dark + High Contrast variants),
shared by app, widget, and share extension via the synchronized `Shared`
group. Every status colour clears WCAG AA on its page, its card, and its
badge surface, in both schemes; measured, not asserted. The accent is now
blue (`#0000FF` light / `#4DA3FF` dark), split into foreground
(`StatusInfo`), fill (`AccentFill`), and on-fill (`AppOnAccent`) roles
because one value cannot serve as both text-on-dark and fill-behind-white.
`AppStatusTone` pairs each status foreground with an authored surface.
- **Light mode unlocked** — the 16 scattered `.preferredColorScheme(.dark)`
calls removed; appearance (System/Light/Dark) is applied once at the
`WindowGroup` and exposed under Settings → Display. `.secondary` (3.29:1 on a
light card) replaced with `AppTextSecondary` across 191 sites.
- **Dynamic Type & reflow** — `Label`-clipped empty-state titles fixed, the
44pt tap-target floor enforced (`AppCopyButton` was 30×30;
`controlMinHeight` was 42), `CardView`'s horizontal-scroll default flipped
to reflow, dense rows (`WatchlistRowView`, `BatchResultRowView`,
`PortfolioExpiryRow`) and the collapsible section headers rebuilt on
`ViewThatFits` so badges and buttons can never letter-wrap vertically, and
the widget clamped at `accessibility1` (fixed canvas, no scroll).
- **VoiceOver** — labels on every icon-only control (label-in-name preserved
for Voice Control), selected-state on all toggles, badges read as one word,
heading-rotor navigation, dense rows collapsed to one element with the
detail on the More Content rotor (`accessibilityCustomContent`), technical
strings (DNS records, cipher suites) spoken with punctuation, and lookup/
sweep completion announcements. Widget rows read as a single phrase.
- **Colour independence, motion, transparency** — widget status uses the badge
symbol vocabulary instead of colour-only dots; `differentiateWithoutColor`
adds symbols/borders on demand; all five animation sites honour
`reduceMotion`; the one material honours `reduceTransparency`.
- **Accessibility audit harness** — `DomainDigUITests` runs
`performAccessibilityAudit()` over every primary screen at default and
AccessibilityXXXL, on CI (newest runtime, clean merge-result checkout) and
locally (`Scripts/audit-a11y.sh`, real floor runtime, wired to an opt-in
pre-push hook). `DOMAIN_DIG_SEED_FIXTURES` seeds deterministic in-memory
rows so the dense paths actually render under audit. The **enforcement
ratchet is engaged**: named findings in six categories fail CI, with
narrowly characterised, always-logged noise suppressions. Manual
verification checklist in `Docs/ACCESSIBILITY_VERIFICATION.md`; findings
burndown 20 → 11 with every remaining item characterised as system noise.
- **Swift 6 language mode** (#27) — all three product targets build under
`SWIFT_VERSION = 6.0` with zero warnings. `SMTPChannel` became an actor
(fixing a real `CheckedContinuation` double-resume hazard),
`SweepActivityController` stores a Sendable activity id, and the remaining
isolation issues were resolved layer by layer. The UITests target stays on
Swift 5 (XCTest override isolation), recorded as a decision.
- **Project hygiene** — the misleading project-level deployment target
(26.2 shadowing the real 17.6) reconciled; CI selects simulators
floor-aware instead of first-match.
Deferred: the Phase 6 manual device passes (VoiceOver walkthrough, Voice
Control, iPad Full Keyboard Access, Liquid Glass runtime check) are tracked in
`Docs/ACCESSIBILITY_VERIFICATION.md` and #21 — they close on device, not in CI.
## v5.0.0 Major: Contract Stabilization & Engineering Health
Goal: earn long-term compatibility promises — and pay down the debt that the
feature releases above will accumulate.
- Define migration policy for persisted snapshots, backups, audits, workflows,
and settings.
- Stabilize the public local API response contract; document compatibility
guarantees and planned deprecations.
- **Extend the test net to the deterministic core.** v4.9.0 established
`DomainDigUITests` (accessibility audit + enforcement gate); unit coverage of
`DomainReportBuilder`, `DomainReportExporter`, `DomainDataPortabilityService`
(merge/replace dedup), and `DiffService` is still needed before locking down
external contracts.
- **Decompose the god-files** behind that test net: `DomainViewModel.swift`
(~4.7k lines) and `ContentView.swift` (~3.8k lines) into focused units
(audit, monitoring, workflows, portability).
## Cross-cutting note
New feature surfaces (widgets, intents, extensions) each add a target and a
persistence/entitlement seam. This project still has **no XCTest target** —
v4.5.0 through v4.7.0 all shipped without the characterization-test safety net
originally recommended before v4.7.0. That gap is now larger (comparison,
reputation, and tags/saved-views all touch persisted models with hand-written
backward-compatible decoders) and should be the very first thing v5.0.0 does,
not a later item within it.
|