# iOS Coding Standard

The one document to read before your first PR. Rule IDs in brackets point at `rules.yml`, which
carries severity, tooling and rationale for each; `EXAMPLES.md` carries a worked ✗/✓ pair for
every rule a tool cannot decide  -  look one up by ID when a review cites it. Your module's own docs
win where they differ  -  this is the floor, not the ceiling.

Five principles, in the order they break things: **security · testability · readability ·
flexibility · consistency.**

---

## 0. The ten lines

1. Sensitive data goes in the Keychain, never in `UserDefaults`, and never into a log. `[SEC-01, SEC-03]`
2. Never reach for the environment  -  inject time, storage, randomness, session. `[TEST-01]`
3. One type per file, named after it; split its concerns with `// MARK:`  -  business rules, service
   calls and UI never share a section. `[STRUCT-01, READ-01]`
4. A screen is a known set of files, always the same set. `[STRUCT-02]`
5. Where a type lives is decided by how many things use it. `[STRUCT-05]`
6. A feature module never imports another feature module. `[MOD-01]`
7. Everything is `private` and `final` until something forces otherwise. `[VIS-01, VIS-02]`
8. One request in, one outcome out  -  `async`, no completion handlers, no `throws` beside a result,
   and the outcome is one closed enum the caller must handle exhaustively. `[SVC-01, SVC-09]`
9. A business rule has a name and a home a test can reach without a view. `[RULE-01, SVC-08]`
10. Variants are configuration, not `if` trees  -  and a screen's load state is one enum carrying its
    payload, not four flags that permit impossible combinations. `[FLEX-02, NAME-06]`

---

## 1. Security

Work out which **data class** a value belongs to before you decide where it goes.
`rules.yml → sensitive_data_classes` is the list: auth token, credential, government ID, travel
document, booking reference, membership identity, payment instrument, personal contact,
biometric/health, precise location. The class decides the storage, not convenience.

### Storage `[SEC-01]`  -  ask "does it persist?" before "where does it go?"

Keychain answers *where a persisted secret lives*. It does not answer *this value is sensitive*.
Most sensitive values in a flow are used and dropped, and those must not be persisted at all.

```swift
// ✗ over-persistence  -  a value the flow throws away in 30 seconds now outlives logout
//   and nobody owns deleting it
try credentialStore.save(oneTimeCode, for: .verificationCode)
UserDefaults.standard.set(passportNumber, forKey: "apisPassport")
```
```swift
// ✓ transient: held by the flow's model, gone when the flow ends
@Observable final class APISFormViewModel {
    private var passportNumber: String = ""      // never leaves memory
}
```
```swift
// ✓ persisted, because it must survive an app restart  -  Keychain, explicit accessibility,
//   no iCloud sync
try credentialStore.save(
    membershipToken,
    for: .authToken,
    accessibility: .whenUnlockedThisDeviceOnly
)
```

**The ladder, in order:**

1. **Does it need to outlive this flow?** Assume no. In-flow data  -  a form field, a scanned
   document number, a one-time code, a draft  -  stays in memory and dies with the flow.
2. **If yes: survive what?** Backgrounding → still just model state. App restart → Keychain.
   Reinstall → a product decision someone signs off, never a storage default.
3. **Which class is it?** `payment-instrument`, `biometric-or-health` and `precise-location`
   stay transient no matter what step 2 said  -  the answer to "it must survive" there is a
   server-side or system token, not local storage.

**Why both directions are findings:** `UserDefaults` is a plist in the app container  -  it lands in
unencrypted backups and outlives the session, so under-protection is obvious. Over-persistence is
the subtler one: an unnecessary Keychain item survives the flow, survives logout unless someone
remembers to delete it, and creates a cleanup obligation with no owner. "Put it in the Keychain to
be safe" is not a safe default.

**Transient is not unregulated.** A value held only in memory is still never logged `[SEC-03]`,
still cleared when the session drops and hidden from the app-switcher snapshot `[SEC-05]`, and
still never sent to analytics `[SEC-06]`.

### Logging `[SEC-03]`

```swift
// ✗ three violations: print, a token in the message, a raw body
print("order response: \(response)")
logger.debug("token=\(token)")
```
```swift
// ✓ private by default; only provably non-sensitive values are public
logger.debug("order completed for \(itemCount, privacy: .public) items")
```
**Why:** device logs are readable by other tooling and are collected in diagnostics. Defaulting to
private means a forgotten annotation fails safe; defaulting to public means it fails open.

### The rest, briefly

- No secret in source  -  anything committed is already leaked. `[SEC-02]`
- HTTPS only; an ATS exception carries a written reason and an expiry date. `[SEC-04]`
- Sensitive data has a lifetime: cleared on logout, hidden from the app-switcher snapshot,
  not cached to disk by default. `[SEC-05]`
- Analytics events, user properties and crash breadcrumbs are redacted  -  check the parameter
  list of every event you add. `[SEC-06]`

  Redaction at the call site is a habit, not a mechanism: it works today and fails on the event
  somebody adds next month. Three properties make it real. **One named helper** does the reduction,
  so there is a single implementation to review and one thing to grep. **The typed event declares
  only the reduced form**  -  a parameter that *can* hold the raw value eventually does. **And the
  reduction has a test:** deterministic for the same input, different for different inputs, and the
  output does not contain the input. Three cheap assertions that turn a claim into a property; an
  untested reduction helper is a finding even when the code is correct. Truncation needs one extra
  check hashing does not  -  that what remains cannot identify the subject alone, and cannot be joined
  against another field in the same payload to do so.
- Permissions are least-privilege with honest purpose strings; the privacy manifest matches what
  you actually collect. `[SEC-07, SEC-08]`
- Debug menus, mock launch arguments and redirect shortcuts are **compiled out** of release, not
  hidden behind a flag. `[SEC-09]`

---

## 2. Testability

Testability is a property of the production code. You cannot add it later by writing tests.

### Inject the environment `[TEST-01]`

```swift
// ✗ untestable by construction  -  the assertion depends on today's date
struct BoardingEligibility {
    func canCheckIn(flight: Flight) -> Bool {
        Date() > flight.departure.addingTimeInterval(-24 * 3600)
    }
}
```
```swift
// ✓ the seam is one parameter wide
struct BoardingEligibility {
    let now: () -> Date

    func canCheckIn(flight: Flight) -> Bool {
        now() > flight.departure.addingTimeInterval(-24 * 3600)
    }
}
```
**Why:** the rule is not "avoid `Date()`"  -  it is that anything the outside world decides
(time, randomness, identifiers, locale, storage, session, feature flags) must be something the
test can decide instead. A type that reaches for it has no seam, and no test discipline recovers.

### Keep the rule callable `[TEST-02, TEST-03]`

```swift
// ✗ the rule is trapped inside the view model, behind a network call and a singleton
func submit() async {
    guard Session.shared.isLoggedIn, passengers.allSatisfy(\.hasDocument) else { return }
    ...
}
```
```swift
// ✓ the decision is a value-returning function; the view model orchestrates
func submitGate(for passengers: [Passenger], isLoggedIn: Bool) -> SubmitGate {
    guard isLoggedIn else { return .requiresLogin }
    guard passengers.allSatisfy(\.hasDocument) else { return .missingDocuments }
    return .allowed
}
```
**Why:** the second version is one line to test and reads as the business rule it is. The first
needs a session, a network stub and a view model instance to answer "what happens when a document
is missing".

Also: name test doubles for what they do  -  stub, spy, fake, mock, **builder**  -  one kind per file,
and keep the signature identical to the real type, because a drifted double is the first thing the
next person copies. A builder is the one most often missing: a base value plus chainable
single-field overrides, so each test states only what it varies instead of restating a fifteen-field
initialiser. `[TEST-04, SVC-02]`

### Give the rule a name and a home `[RULE-01]`

Three homes, and the ladder picks. **One screen asks it** → the view model; stop there, a namespace
with one consumer is over-hoisting. **Several screens ask it of data the entity already carries** →
a computed property on the entity, under `// MARK: - Derived`. **Several screens ask it but the
answer is screen policy rather than entity state** → a named rule namespace.

```swift
// ✗ a private computed property on the scene; the next screen needing this re-derives it
private var showsStrikethrough: Bool { viewModel.segment.historyInfo?.crossed == true }
```
```swift
// ✓ a caseless enum of pure statics in the domain layer, each citing what it enforces
enum ReservationRules {
    /// BR-21: the overline strikes ONLY when `historyInfo.crossed` is true. When it is false the
    /// history fields may still be populated (voluntary itinerary metadata)  -  do not strike then.
    static func shouldRenderStrikethrough(_ segment: Segment) -> Bool {
        segment.historyInfo?.crossed == true
    }
}
```
```swift
// ✓ and the test repeats the citation token, so one grep reaches spec, code and test
@Test("BR_21: no strikethrough when the segment is not crossed")
func test_BR_21_noStrikethrough_whenNotCrossed() { ... }
```
**Why:** the citation is not decoration, it is the rule's own measurement. A namespace whose
functions cite nothing cannot be checked against anything, and a cited requirement with no test of
the same token is an untested requirement. What the namespace must NOT become is a Utils bucket: a
value transform is `READ-04d`, a shape lowering is `SVC-08`, a form rule is `STRUCT-06`.

### A null object may ship; a test double may not `[TEST-08]`

A type that satisfies a protocol by doing nothing  -  no event sent, `nil` returned, empty list  -  is
the honest default for a dependency a caller has legitimately not wired: a defaulted initialiser
parameter, a canvas preview, an optional slot nobody configured yet. Name it consistently and let it
live in production sources.

A type that carries **canned payloads** does not. It ships sample data to users, grows the store
binary, and can be resolved by accident because nothing but its name says it is not real. It belongs
in the test target. Distinguish by payload, never by name: a `Mock` that returns nothing is a null
object misnamed, and a `Noop` returning three sample records is a double in the wrong target. If a
canned payload really is needed at runtime  -  a demo build, an offline mode  -  that is a debug
affordance and `SEC-09` asks what removes it from the store build.

---

## 3. Readability

### Separate concerns with MARKs `[READ-01]`

Business rules, service calls and UI never share a section. A reader must find each without
reading the file. ViewModel order: `Properties → Init → Derived state → Flow → Gate → Intents →
Error handling`. Scene order: `State → Init → Body →` one `@ViewBuilder` per visual section.

### Extract by call-site count, not by feel `[READ-04]`

Two or more call sites → its own file with its own configuration. Exactly one call site and bound
to the screen's state → a `private @ViewBuilder` in a MARK'd extension. Pushing a state-coupled
fragment into its own file to shrink the screen trades one long file for a file plus a binding
tangle  -  that reads worse, and it is a finding in the same way the opposite is.

### A pure transform is a shared helper `[READ-04d]`

```swift
// ✗ a date formatter living on the scene that happened to need it first
extension PassengerAndFlightSelectionScene {
    func formattedDate(_ raw: String?) -> String { ... }   // and again, later, in a mapper
}
```
```swift
// ✓ one home for value transforms
enum OrderBFFFormatters {
    static func displayDate(fromISODay raw: String?) -> String { ... }
    static func initials(from name: String?) -> String? { ... }
}
```
**Why:** these have no screen state, so nothing ties them to a screen  -  and left where they were
typed they get written a second time somewhere else, with a slightly different edge case.

### A view fragment that renders a thing is a component file `[READ-04b]`

```swift
// ✗ a component hiding as a computed property: cannot be previewed, cannot be reused,
//   and it reads the whole view model so it never could be
private var legSwitcher: some View {
    HStack {
        ForEach(viewModel.segments) { segment in
            Button { Task { await viewModel.onSegmentSelected(segment.segmentIndex) } } label: { ... }
        }
    }
}
```
```swift
// ✓ SeatMapLegSwitcher.swift  -  data in, callbacks out, previewable on its own
struct SeatMapLegSwitcher: View {
    let segments: [SeatMapSegment]
    let activeSegmentIndex: Int
    let onSelect: (Int) -> Void
    var body: some View { ... }
}

#Preview {
    SeatMapLegSwitcher(segments: [.gidis, .donus], activeSegmentIndex: 0, onSelect: { _ in })
}

// ✓ the scene keeps the composition  -  which component shows, in what order
@ViewBuilder
var content: some View {
    if viewModel.loadError { errorRetryView } else { SeatMapLegSwitcher(...) }
}
```
**Why:** the canvas is the fastest way to check a visual piece, and it only works when the piece
takes data. A fragment bound to a view model needs the DI container no preview configures  -  so
it never gets looked at until the whole flow is run on a device.

### One type per file; nest only owned details `[STRUCT-01]`

```swift
// ✗ a response model nested inside another  -  invisible to a filename search,
//   and moving it later renames every reference
struct OrderResponseModel {
    struct PassengerModel { ... }
}
```
```swift
// ✓ data types are top level, one per file
struct OrderResponseModel { let items: [OrderItemModel] }   // OrderResponseModel.swift
struct PassengerModel { ... }                                      // PassengerModel.swift

// ✓ still fine  -  an owned detail with exactly one owner
@Observable final class SeatMapViewModel {
    enum ViewState { case loading, loaded, failed }
}

// ✓ also fine  -  a pure constants namespace; the nesting IS the grouping
enum AppConstant {
    enum Phone { static let defaultDialCode = "+90" }
    enum DeepLink { static let scheme = "myapp" }
}
```
**Why:** entities and transport models get looked up by name, move between placement tiers as
consumers change, and are referenced from mappers and tests. A `ViewState` or a `static let`
literal does none of that  -  flattening `AppConstant.Phone` to `AppConstantPhone` loses the
grouping and buys no discoverability.

### A method that wraps one service is named after it `[SVC-07]`

```swift
// ✗ transport verbs invented by the client  -  the name says what the code does
//   (which the signature already says), not which service will fire
func fetchOpenStatus(_ request: CheckOpenStatusRequestModel) async -> Result
func loadPassengers(...) async -> Result
```
```swift
// ✓ `send` + the endpoint path, segments in the backend's own order
func sendCheckOpenStatus(_ request: CheckOpenStatusRequestModel) async -> Result  // check-open-status
func sendOrderItemsSave(...) async -> Result                                      // order/items/save
func sendSeatExtend() async -> Result                                             // seat/extend

// ✗ the generated client's method name is not the service name: `get` is the generator's
//   HTTP-verb prefix and it reorders the path the backend chose
func sendGetSeatMapPageInfo() async -> Result   // seat/map-page-info -> sendSeatMapPageInfo

// ✓ variants over one service: the single caller is named, the variants sit above it
func sendGetCountryList() async throws -> CountryLookupResponse   // private, the one call
func fetchNationalityList() async -> Result  // standard:exception(SVC-07) screen variant
func fetchAreaCodeList() async -> Result     // standard:exception(SVC-07) screen variant
```
**Why:** one grep from the endpoint reaches every layer that touches it, and the call site
tells you which service fires without opening the repository. It also survives the rename the
other way round: when the backend renames an endpoint, the compiler shows you every screen.

**This one is a module decision, not a universal.** The alternative  -  naming the method after what
the domain asks for  -  reads better at the call site and survives an endpoint rename, at the cost of
that grep. Both are defensible, so the rule binds to the `ServiceNamingScheme` overlay slot: it is
active only where a module declared `send-path`, and what it then enforces is consistency with the
module's own declared scheme, never conformity to a sibling's choice. Check the module's overlay
before raising this. Same for the navigation-exit spelling `[NAME-01]` and the component directory's
name `[READ-04b]`  -  see `rules.yml → module_overlay_slots`.

### A mapper moves values; it never decides `[SVC-08]`

```swift
// ✗ three decisions hiding in a lowering: a unit conversion, a clamp, and a screen state
struct SummaryMapper: Sendable {
    func map(dto: SummaryDto) -> SummaryData {
        SummaryData(
            sessionTimeout: dto.sessionTimeout.map { TimeInterval($0) / 1000.0 },
            variant: dto.info?.status == .error ? .flightError : .standard,
            otpTimeout: dto.timeout ?? 180
        )
    }
}
```
```swift
// ✓ the mapper carries the wire facts, unit in the name
SummaryData(sessionTimeoutMs: dto.sessionTimeout, isFailure: dto.info?.status == .error,
            otpTimeout: dto.timeout)

// ✓ the view model decides, in its own business-rules section
var variant: SummaryVariant {
    if data.isFailure { return anyApisRedirect ? .apisError : .flightError }
    return data.isMultiSegment ? .oneStop : .standard
}
```
**Why:** the mapper is the one type with no screen context. A rule buried in it is invisible
from the view model that owns the behaviour, untestable without hand-building a DTO, and quietly
duplicated the next time another screen needs the same decision. `?? ""` on an optional wire
field is not a decision  -  it is the lowering itself.

**Moving a rule out of a mapper is two edits, not one.** The reason these survive review is that
deleting the decision also deletes the data it was computed from: the mapper stored `isFailure` and
dropped `info.status`, so no later layer can re-derive it. First make the entity carry the raw
inputs, then express the decision  -  and put it in one of the three homes `[RULE-01]` names, never as
a stored field the mapper fills, because a stored field is indistinguishable from a wire value at
every call site that reads it.

### Signatures read as the contract `[SVC-01]`

```swift
// ✗ the parameter list is a request model nobody wrote
func savePassengers(pnr: String, surname: String, passengers: [Passenger],
                    contact: ContactInfo?, acceptsTerms: Bool) async throws -> Bool
```
```swift
// ✓
public func savePassengers(
    _ request: SaveOrderItemsRequestModel
) async -> OrderServiceResult<SaveOrderItemsResponseModel> {
```
**Why:** one request model means adding a field touches one type instead of every caller; one
result family means one error channel instead of `throws` plus a result plus an optional. Past
~2 parameters at a service boundary, the parameters want to be a model.

### The outcome is a closed enum `[SVC-09, SVC-05]`

```swift
// ✗ two outcome channels, so no switch over the result is ever exhaustive
func checkAccess(identifier: String) async throws -> AccessResponse
```
```swift
// ✓ one type enumerating every outcome the caller must handle
enum AccessOutcome: Equatable, Sendable {
    case dashboard(pnr: String?)                 // a success case may name a DESTINATION
    case ticketEntrance(pnr: String?)
    case serviceError(referenceCode: String?, message: String?)
    case offline
    case timeout
}

// ✓ and the transport error is projected in the data layer, never above it
} catch let error as ServiceError {
    return error.projected(offline: .offline, timeout: .timeout) {
        .serviceError(referenceCode: $0, message: $1)
    }
}
```
**Why:** the compiler answers "is any outcome unhandled?", which no `catch` can, and a success case
naming a destination is what this buys over a bare `Result<Payload, Error>`  -  the wire's redirect
string is resolved into a case in the repository, so no raw string reaches the view model.

**Where the line falls between this and `SVC-05`.** How a call can FAIL is a module fact: one shared
failure vocabulary, one factory that projects the transport error onto it. What a screen does NEXT is
a screen fact: its own outcome enum, with its own destinations. A screen enum is correct when its
failure cases name the shared kinds; it is the violation when it respells them. Re-declaring the same
offline / timeout / server triple in a dozen screen enums means a new failure kind is a dozen edits
and the classifications drift apart  -  which is the `SVC-05` finding, not an argument against this
shape.

### The load state is one enum with its payload attached `[NAME-06]`

```swift
// ✗ four properties; `isLoading` with data present and an error set is a state nothing rejects
var isLoading = false
var reservation: Reservation?
var errorMessage: String?
var isOffline = false
```
```swift
// ✓ the payload hangs off the case, and the enum answers questions so the view never destructures
struct LoadedContent: Hashable {
    let reservation: Reservation
    /// `nil` when the passenger request failed but the reservation loaded  -  the screen still
    /// renders its flights, the passenger area is simply not drawn.
    let passengerArea: PassengerArea?
}

enum ScreenState: Hashable {
    case idle, loading
    case loaded(LoadedContent)
    case error(ScreenError)

    var isLoading: Bool { if case .loading = self { return true }; return false }
}
```
Two more things to copy. **State the case you deliberately did not model, in the file:** "there is no
`empty` case  -  a successful response always represents a populated record; an empty payload arrives
as a not-found error." Without that line the next reader cannot tell a considered omission from an
oversight and adds the case defensively. And **a secondary source that may fail is an optional slot
inside the payload, not a second state**  -  say what `nil` means at the property. Promoting a
non-critical failure to a screen-level error discards the data that did arrive; silently defaulting
it to an empty value makes "absent" and "empty" indistinguishable.

### Visibility is documentation `[VIS-01, VIS-02, VIS-04]`

Everything `private` and `final` until something forces otherwise. `public` only on what another
module actually imports  -  and every surviving `public`, plus every cross-module contract, carries
a doc comment. That is the one place the "no unnecessary comments" rule inverts: an
implementation detail explains itself through naming, a contract between two teams cannot.

---

## 4. Flexibility

### A screen is a known file manifest `[STRUCT-02, STRUCT-03]`

`Scene · ViewModel · LoadState · CopySurface · NavigationExit · AnalyticsTracking · UseCase ·
Repository (+protocol) · Mapper + models`  -  each present when its responsibility exists, absent when
it does not. An empty copy surface on a screen with no copy is noise, not compliance. Every screen
sits at the same depth with the same internal grouping, because people navigate by muscle memory.

Two of those names are the module's to spell. The navigation-exit type is a `CoordinatorEvent` enum
with a handler alias in some modules and an `Output` enum with a closure in others  -  both enumerate
every exit in one place, which is the property that matters `[NAME-01]`. The test double is not on
the list at all: it lives in the test target `[TEST-08]`.

**Where the protocol/implementation split earns its keep `[STRUCT-08]`.** An implementation with a
body  -  a repository doing request construction, mapping, cache reads, error projection  -  goes in its
own file beside its protocol, so a reader opening the contract does not scroll past it. A use case
that is a pure pass-through, forwarding one call to one collaborator, may keep protocol, live type
and null object in one file: there is nothing to scroll past, and splitting forty of those produces
eighty files whose names differ by a suffix. Decide by whether there is anything a reader must skip,
never by the layer's name  -  and answer the same way across the module, because the cost of this rule
is a reader guessing which file a type is in. (A pass-through use case also invites `SVC-01`'s
question: what does it add over calling the repository? Separate finding.)

### Placement is a consumer count `[STRUCT-05]`

| Consumers | Home |
|---|---|
| 2+ modules | cross-module shared tier |
| 2+ screens | the module's shared entities |
| one screen | that screen's own folder  -  **not** the shared tier |
| one type | its own file beside that type |

Both directions are findings. A single-consumer type parked in the shared tier inflates the shared
surface and makes the next reader think it is load-bearing.

### Modules do not know each other `[MOD-01, MOD-05, MOD-06]`

A feature module never imports a sibling feature. Cross-feature needs go through the seam layer
(contracts / bridges / adapters). DI resolves a protocol declared in core or the seam  -  resolving
another feature's concrete type is a compile-time dependency in a runtime disguise. Only the
composition root knows the module list.

The test: **removing this module should touch only the composition root.** If a sibling feature
mentions it, it is not a module, it is a folder.

### Variants are configuration `[FLEX-02, FLEX-03]`

```swift
// ✗ branches that differ only in tokens and copy
if isCompact { Text(title).font(.body).padding(8) }
else { Text(title).font(.title).padding(16) }
```
```swift
// ✓ one path, a configuration value
Text(title)
    .typographyStyle(style.typography)
    .padding(style.padding)
```
**Why:** adding a third variant costs a case instead of a branch, and the component stays open for
extension  -  the next design change does not edit its body.

---

## 5. Concurrency

Applies where the module is in Swift 6 language mode; the compiler already blocks the unsafe
cases, so these rules are about the model being *legible*.

- The isolation policy is one decision applied everywhere. A reader must know where a function
  runs from its declaration, without tracing callers. `[CONC-01]`
- State `Sendable` where it is load-bearing, consistently. `[CONC-02]`
- `@preconcurrency`, `nonisolated(unsafe)` and `@unchecked Sendable` are migration tools. Each
  needs a reason and a removal condition  -  they are counted, and a rising count means the module
  is quietly returning to pre-Swift-6 guarantees while the build stays green. `[CONC-03]`
- One model: no `DispatchQueue`, semaphore or completion handler layered onto `async`. `[CONC-04]`
- Every task has an owner and a cancellation story. An unowned task outlives its screen and writes
  to dead state. `[CONC-05]`

---

## 6. Accessibility

Identifier from the shared source on every interactive element `[A11Y-01]` · localized VoiceOver
label, plus a hint when the action is not obvious from the label `[A11Y-02]` · 44×44 minimum tap
target, and grouped content exposes one meaningful element rather than five fragments `[A11Y-03]`
· Dynamic Type survives the largest accessibility sizes  -  no fixed-height container around
scalable text `[A11Y-04]` · RTL mirrors, so `leading`/`trailing`, never `left`/`right` `[A11Y-05]`.

---

## 7. Performance  -  the four that are also readability

No expensive computation in a view body `[PERF-01]` · lazy containers with stable identity, never
index-as-id `[PERF-02]` · no blocking work at init or on the main actor `[PERF-03]` · no formatter,
calendar or regex constructed per render `[PERF-04]`.

**Explicitly out of scope of this standard:** Instruments-driven optimisation, launch-time budgets,
memory profiling. Those belong to a performance workflow  -  measure before optimising, and do not
let a style document push you into speculative tuning.

---

## 8. Exceptions

A rule you cannot follow is fine; an undocumented one is not. Mark it in code:

```swift
// standard:exception(SEC-04) legacy partner endpoint pending TLS migration 2026-12-31
```

The linter honours the marker, the audit counts it, and the count is reviewed. Unwritten
exceptions become silent decay; counted ones become a backlog.
