<feed xmlns='http://www.w3.org/2005/Atom'>
<title>domain-dig.git/DomainDig.xcodeproj/project.pbxproj, branch v5.0.1</title>
<subtitle>local-first ios domain inspection toolkit. dns, tls, rdap, audit.
</subtitle>
<id>http://git.krz.sh/krz/domain-dig.git/atom?h=v5.0.1</id>
<link rel='self' href='http://git.krz.sh/krz/domain-dig.git/atom?h=v5.0.1'/>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/domain-dig.git/'/>
<updated>2026-07-25T17:19:37+00:00</updated>
<entry>
<title>feat: owner Pro+ allowlist via CloudKit, cut v5.0.1</title>
<updated>2026-07-25T17:19:37+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-25T17:14:58+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/domain-dig.git/commit/?id=963ce3fb34bd6c6bd72f1aa956ce602267b90e9c'/>
<id>urn:sha1:963ce3fb34bd6c6bd72f1aa956ce602267b90e9c</id>
<content type='text'>
Grants the app owner Pro+ without a purchase, keyed to their CloudKit
user-record ID so it works on the release App Store build.

- OwnerAccess holds the owner's CloudKit user-record ID (opaque, per-Apple-ID,
  scoped to the app's container; safe to publish — CloudKit verifies identity
  server-side, so it can't be presented by anyone else).
- PurchaseService resolves the allowlist against CloudKit once per launch and,
  on a match, records a persisted owner grant so it applies instantly and
  offline thereafter. The grant only ever elevates the tier to .proPlus and
  defers to the existing #if DEBUG overrides, so real purchases and free/pro
  testing are unaffected. cachedTier / cachedEntitlement were refactored to
  fall back to the owner grant only when no debug override or stored purchase
  applies.
- Supersedes the DEBUG record-ID reveal (PR #60): its only purpose was to read
  the owner's ID, which is now hardcoded, so the reveal is not shipped.

Release cut: MARKETING_VERSION 5.0.0 -&gt; 5.0.1, CURRENT_PROJECT_VERSION 45 -&gt; 46,
AppVersion.current -&gt; 5.0.1, roadmap updated. App builds clean; unit suite 63/63.
</content>
</entry>
<entry>
<title>feat: enrich Settings → App Info with metadata &amp; resource links (#56)</title>
<updated>2026-07-25T16:49:23+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-25T16:35:46+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/domain-dig.git/commit/?id=074052763638a36e274e18d00f5f5ab21846be28'/>
<id>urn:sha1:074052763638a36e274e18d00f5f5ab21846be28</id>
<content type='text'>
Replaces the three-row App Info screen with a full About/Resources/Support/
Legal layout, driven by a declarative AppInfoRow model with all URLs centralized
in one AppLinks namespace.

- About: Version now shows "5.0.0 (build N)" (CFBundleShortVersionString +
  CFBundleVersion), plus Storage, Backup Schema, and Minimum iOS (17.6).
- Resources: What's New (bundled ReleaseNotes.json sheet, no network),
  Documentation &amp; FAQ, Source Code, Privacy Policy, and Acknowledgements
  ("no third-party dependencies" + MIT license).
- Support: Report an Issue (a sheet that shows the locally-assembled
  version/OS/device diagnostics before offering Email or GitHub — nothing is
  collected silently) and Contact (mailto).
- Support the App: Rate (SwiftUI's @Environment(\.requestReview), which handles
  the scene internally and respects Apple's throttling — the modern, safer
  equivalent of the issue's SKStoreReviewController + connectedScenes path) and
  Share (ShareLink to the App Store listing).
- Legal: copyright footer with the current year.

External rows use Link/openURL with an "opens outside the app" accessibility
hint; the screen is standard adaptive Form controls with semantic colors, so it
tracks Dynamic Type and light/dark automatically.

New files (AppLinks, AppInfo, ReleaseNotes[.swift/.json], AppInfoView) auto-
compile/bundle via the synchronized DomainDig/ group. AppInfoTests covers the
mailto builder, version format, diagnostics contents, and that the bundled notes
ship and parse. Unit suite green.

Placeholder pending the App Store listing: AppLinks.appStoreID (the review/share
URLs derive from it), flagged with a TODO.
</content>
</entry>
<entry>
<title>chore: cut v5.0.0 release (version bump)</title>
<updated>2026-07-25T16:18:41+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-25T05:31:51+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/domain-dig.git/commit/?id=cc1cb343b19b662ef59f9700702ed285e4591029'/>
<id>urn:sha1:cc1cb343b19b662ef59f9700702ed285e4591029</id>
<content type='text'>
Bumps the app to 5.0.0 now that all four v5.0.0 workstreams have landed:

- AppVersion.current 4.9.0 -&gt; 5.0.0
- MARKETING_VERSION 4.9.0 -&gt; 5.0.0 across all targets (app, widget, share
  extension, and both test bundles)
- CURRENT_PROJECT_VERSION 44 -&gt; 45 across all targets
- RELEASE_ROADMAP.md: v5.0.0 marked shipped, "Current version" -&gt; v5.0.0

The three version sources are kept in lockstep, matching the v4.4.1 alignment
policy. App Store archive/submit remains a manual step outside the repo.
</content>
</entry>
<entry>
<title>feat: versioned store-migration policy for persisted data (v5 step 2)</title>
<updated>2026-07-25T04:37:00+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-25T04:35:18+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/domain-dig.git/commit/?id=99e4623af1b08f36120a01b67cbe60df99668651'/>
<id>urn:sha1:99e4623af1b08f36120a01b67cbe60df99668651</id>
<content type='text'>
Third v5.0.0 roadmap item: define and implement a migration policy for the
on-device persisted store (tracked domains, history/snapshots, audits,
workflows, monitoring, settings), so data upgrades cleanly across app versions
instead of relying on a one-shot marker.

- DataMigrationService is reworked from a single boolean marker
  (`data.migrations.v3_4_0`) into a versioned runner keyed by an integer store
  schema version (`data.storeSchemaVersion`). It runs each step once in
  ascending order up to `currentStoreSchemaVersion`, stamping the version as it
  goes. Adding a future migration is now a `case N:` plus a version bump.

  Policy guarantees, all covered by tests:
  - Forward-only and idempotent; every step must be safe on an empty/older store.
  - Never downgrades: a store written by a newer build (higher version) is left
    byte-for-byte untouched.
  - Pre-versioning installs are handled: a set legacy boolean marker reads as
    "already at v1", so the v1 normalization never re-runs for them.

  v1 is the existing normalization pass (dedup + drop the legacy `watchedDomains`
  key + sanitize monitoring settings), now expressed as migration step 1.

- Docs/data-migration.md documents the persisted surface, the two independent
  version lines (store vs. backup export), when to use lenient decoding vs. a
  migration step, the runner contract, an "adding a migration" checklist, and
  backup-import compatibility. Linked from the README.

- DataMigrationServiceTests: 6 tests over legacy fixtures — fresh-store stamping,
  legacy `watchedDomains` migration + key drop, in-place dedup of the stored
  blob, idempotence, legacy-marker-as-v1, and the no-downgrade guard. Full unit
  suite: 58 passing.
</content>
</entry>
<entry>
<title>feat: stabilize and document the Local API v1 response contract (v5 step 3)</title>
<updated>2026-07-25T04:29:01+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-25T04:14:23+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/domain-dig.git/commit/?id=a52dee116d4066d1b59bd90b4ebc4def4e1597d6'/>
<id>urn:sha1:a52dee116d4066d1b59bd90b4ebc4def4e1597d6</id>
<content type='text'>
Second v5.0.0 roadmap item: make the Local API's public JSON contract explicit,
documented, and regression-locked, so external consumers (Shortcuts, scripts,
integrations) have a stable surface with a defined compatibility promise.

- LocalAPIContract: new single source of truth for the wire-format version
  ("v1") and the canonical JSON encoder (ISO-8601 dates, sorted keys). Both the
  success and error paths in LocalAPIService now route through it, so the format
  can't drift between them, and the ad-hoc per-call-site encoders are gone.

- The response envelope and every payload struct are promoted from `private` to
  internal so the contract is a first-class, testable part of the module. The
  transport/handler internals (request parser, HTTP response, secret store)
  stay private.

- Docs/local-api.md documents the base URL/auth, the envelope, the encoding
  conventions (notably: absent optionals are omitted, not null), every endpoint
  and its payload fields, the error codes, and the semantic-version-style
  compatibility policy (additive changes keep v1; renames/removals/type changes
  bump the version). Linked from the README.

- LocalAPIContractTests: 16 structure/"golden" tests pinning the envelope shape,
  each payload's field names, the enum encodings, and the ISO-8601 date format.
  They assert structure, not values, so ordinary behavior changes don't churn
  them but a renamed or dropped field fails CI. Full unit suite: 52 passing.
</content>
</entry>
<entry>
<title>test: establish unit-test net for the deterministic core (v5 step 1)</title>
<updated>2026-07-25T03:52:53+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-25T00:27:24+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/domain-dig.git/commit/?id=a7d154e1db9fd9904b2255db889144ad15aac2f2'/>
<id>urn:sha1:a7d154e1db9fd9904b2255db889144ad15aac2f2</id>
<content type='text'>
Stands up the DomainDigTests unit-test target the v5.0.0 roadmap flags as the
mandatory first move before decomposing the god-files and locking external
contracts. The project had no XCTest unit target — only the DomainDigUITests
accessibility suite.

- New DomainDigTests target (unit_test_bundle), hosted by the app so
  @testable import DomainDig links. Mirrors the UITests build settings
  (SWIFT_VERSION 5.0 + MainActor default isolation) to avoid the XCTest
  override-isolation issue recorded for the Swift 6 targets. Wired into the
  shared DomainDig scheme's Test action, so it runs in CI and the pre-push
  audit hook automatically (both invoke the whole scheme).

- SnapshotFixture builds the deep LookupSnapshot/DomainReport models through
  their real initializer and the DomainReportBuilder, exposing only the fields
  the tests vary.

- 36 characterization tests across the four deterministic units the roadmap
  names:
  - DiffService: change classification, case/whitespace normalization, DNS
    record reorder-vs-change, summary phrasing, resolver context note,
    certificate-warning thresholds.
  - DomainReportBuilder: snapshot -&gt; report field mapping, primary-IP and
    DNSSEC derivation, TLS status, partial-snapshot/validation passthrough.
  - DomainReportExporter: format dispatch, JSON round-trip (the machine
    contract), CSV/markdown/text/PDF structural invariants, timeline.
  - DomainDataPortabilityService: CSV round-trip and the merge/dedup semantics
    (case-insensitive collapse, OR-merged pin state, min-created/max-updated,
    recency sort) via an ephemeral UserDefaults suite.

- build.yml comment updated to reflect the second test target.
</content>
</entry>
<entry>
<title>chore: bump to v4.9.0; record the release in the roadmap</title>
<updated>2026-07-23T05:42:28+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-23T04:26:48+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/domain-dig.git/commit/?id=655d11b59a273c14720952478ccebec20a9627bd'/>
<id>urn:sha1:655d11b59a273c14720952478ccebec20a9627bd</id>
<content type='text'>
MARKETING_VERSION 4.8.3 -&gt; 4.9.0 and CURRENT_PROJECT_VERSION 43 -&gt; 44
across all targets, with AppVersion.current aligned — the three-way
consistency v4.4.1 established.

The roadmap gains the v4.9.0 entry: the full accessibility pass (#21
phases 0-5 — semantic colours, light mode and the appearance setting,
Dynamic Type reflow and tap targets, VoiceOver, colour independence and
motion/transparency), the audit harness with seeded fixtures and the
engaged enforcement ratchet, the manual verification checklist, and
Swift 6 language mode adoption (#27). Deferred device passes are named
rather than implied.

The v5.0.0 "establish a test target" bullet is rewritten to match
reality: a UI test target now exists with an enforcement gate; what
remains for v5.0.0 is unit coverage of the deterministic core.
</content>
</entry>
<entry>
<title>fix: adopt Swift 6 language mode; resolve all concurrency issues (#27)</title>
<updated>2026-07-23T04:23:11+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-23T02:43:42+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/domain-dig.git/commit/?id=872583eca8b4e7ae6ef85917604d4a5257b9d125'/>
<id>urn:sha1:872583eca8b4e7ae6ef85917604d4a5257b9d125</id>
<content type='text'>
All three product targets (app, widget, share extension) now build under
SWIFT_VERSION = 6.0 with zero errors and zero warnings. The UITests
target stays on 5.0: XCTestCase's nonisolated setUp/init overrides
conflict with the target's MainActor default isolation under 6, and test
tooling is not shipping code.

The original seven diagnostics, plus the layers Swift 6 mode surfaced
once those cleared:

- SMTPChannel is an actor. It was implicitly MainActor while running its
  receive loop on a background queue, so parsedLines/lineWaiters/
  receiveBuffer were declared main-actor-protected and mutated off it —
  concurrent mutation while resuming a CheckedContinuation can
  double-resume, which traps. The actor serialises all state; Network
  callbacks hop in via Task. The start() continuation also gains an
  OSAllocatedUnfairLock resume-once guard: the state handler can fire
  .ready and later .failed, and resuming twice was a pre-existing trap of
  the same family.
- CachedLookupResult is nonisolated (a value pair built inside actor
  LookupRuntime cannot have a MainActor-bound memberwise init) with
  conditional Sendable — opting out of MainActor isolation also opted out
  of the implicit Sendable that globally-isolated types get.
- PortScanService.printableBanner is nonisolated: a pure transformation
  called from the connection's queue.
- SweepActivityController stores the activity's Sendable id instead of
  the non-Sendable Activity, re-resolving via Activity.activities inside
  each fire-and-forget task, so nothing non-Sendable crosses isolation.
- App Intents' static title/description/openAppWhenRun become lets
  (get-only protocol requirements; static var is shared mutable global
  state), and the summary helpers are @MainActor to match the model
  properties they read and the perform() implementations that call them.
- ExternalDataService's ISO8601DateFormatter is nonisolated(unsafe),
  citing Apple's documented thread-safety, rather than risking a parser
  behaviour change by switching APIs with no test coverage.
- TaskMetricsDelegate.metrics is nonisolated(unsafe): written on the
  session's delegate queue, read only after the request completes, and
  URLSession guarantees didFinishCollecting precedes task completion.
- The share extension extracts the host via async/withCheckedContinuation
  instead of sending a non-Sendable completion into loadItem's @Sendable
  handler; Task inherits the view controller's MainActor so the manual
  DispatchQueue.main hop goes too.

Validated: clean Swift 6 build of all product targets, and the full
enforced 11-test audit suite green on the floor runtime — Swift 6's
runtime isolation checks ran the app through every screen without a
trap.
</content>
</entry>
<entry>
<title>feat(a11y): add accessibility audit harness (#21 phase 0)</title>
<updated>2026-07-20T22:55:24+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-20T22:21:03+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/domain-dig.git/commit/?id=38723030eb0db58a9d62a45c7725861901309a05'/>
<id>urn:sha1:38723030eb0db58a9d62a45c7725861901309a05</id>
<content type='text'>
Phase 0 of the accessibility pass: a regression guard that must exist
before any of the remedial phases, so their acceptance criteria are
enforced rather than asserted once by hand.

- Fix the project-level IPHONEOS_DEPLOYMENT_TARGET, which was 26.2 while
  all three targets are 17.6. It was shadowed everywhere today, but any
  target added later would silently inherit it and drop iOS 17.6 support
  with no error.

- Add a DomainDigUITests target running performAccessibilityAudit on the
  six primary screens, plus a sweep of every root screen at
  AccessibilityXXXL. Uses the existing DOMAIN_DIG_FORCE_PRO_PLUS debug
  argument so Pro-gated screens are reachable.

- Findings are reported, not failed. The audit surfaces violations that
  exist today, so gating on them would block unrelated PRs until the
  whole pass lands. Enforcement is a committed constant,
  AccessibilityAuditHarness.enforcedAuditTypes, widened per audit type as
  each phase clears a category.

- CI now runs xcodebuild test across two simulators. Audit coverage is
  not nested between OS versions: on Tracked Domains, iOS 18.6 reported 2
  findings and iOS 27.0 reported 6 (including contrast and
  element-detection issues 18.6 never raised), while at accessibility
  text sizes the Dashboard produced a hit-region finding on 18.6 that
  27.0 did not.

- Simulator selection is now dynamic and floor-aware. The previous
  selector took the first iPhone from any runtime, which can resolve to a
  simulator below the deployment target where the app cannot install.

Baseline on iOS 18.6: 15 findings across 7 tests — text clipping on every
screen, contrast on Inspect and Settings, and a hit-region failure on the
Dashboard at accessibility text sizes.
</content>
</entry>
<entry>
<title>v4.8.3: Clear SonarCloud new-code issues</title>
<updated>2026-07-20T21:27:59+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-20T21:22:28+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/domain-dig.git/commit/?id=bb18197860ffdb491f010648ec903003b6e1bb65'/>
<id>urn:sha1:bb18197860ffdb491f010648ec903003b6e1bb65</id>
<content type='text'>
Fixes the 4 reported bugs and ~97 code smells flagged in the new-code
period. No behavior changes.

Bugs (swift:S3923) — DomainInspectionService's confidenceFor* helpers each
returned `error == nil ? .low : .low`, an inert conditional. Simplified to
`return .low` and dropped the now-unused `error` parameter.

Smells:
- Merged 14 identical `.empty`/`.error` switch branches in DomainViewModel
- Consolidated duplicate implementations (clearPresentedResults/reset,
  String.nonEmpty/nilIfEmpty, ExportFormat.id/fileExtension)
- Extracted nested ternaries into TLSGrade.tone, EmailSecurityGrade.tone,
  and ChangeImpactClassification.color; removed ContentView.impactColor
  and the duplicate mapping in BatchResultsView
- Documented empty closures and singleton inits
- Marked unused protocol-conformance parameters `_`
- Renamed CloudSyncTrigger.`import` to `imported` (raw value preserved)
  and SSLSessionDelegate's _serverTrust/_tlsMetadata
- Merged nested ifs in the DER parser; flattened closure nesting in
  PortScanService and IntegrationService
- Replaced two-case switches with if/else

Left open: S107 (init parameter counts), S115 (constants mirroring DoH and
ipapi JSON keys), S1075 (false positives on https:// literals), and two
S117 hits on SwiftUI $binding shorthand. These want a Won't Fix resolution
in SonarCloud, not a code change.
</content>
</entry>
</feed>
