@mmerterden/multi-agent-pipeline 13.2.0 → 13.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,17 +6,17 @@ user-invocable: true
6
6
  argument-hint: "[module name or path]"
7
7
  ---
8
8
 
9
- # multi-agent ios-coding-standardModule audit → plan → dev handoff
9
+ # multi-agent ios-coding-standard - Module audit → plan → dev handoff
10
10
 
11
- **Input**: $ARGUMENTSoptionally a module name or path. When absent, Phase 1 discovers and asks.
11
+ **Input**: $ARGUMENTS - optionally a module name or path. When absent, Phase 1 discovers and asks.
12
12
 
13
13
  This routine is the **procedure**. The rules live in the `ios-coding-standard` skill, whose registry is
14
14
  `references/rules.yml` and whose teaching doc is `references/STANDARD.md`. Load that
15
15
  skill first; it is installed on every host and carried by the iOS stack plugin, so
16
- there is one registry rather than a copy per CLI. Never restate a rule herecite its ID. A rule that is not in the
16
+ there is one registry rather than a copy per CLI. Never restate a rule here - cite its ID. A rule that is not in the
17
17
  registry is not a rule; if the audit needs one, propose it as `status: proposed` and say so.
18
18
 
19
- Read-only + planning. This routine never edits sourcethe dev pipeline does.
19
+ Read-only + planning. This routine never edits source - the dev pipeline does.
20
20
 
21
21
  **Goal.** A developer new to the repo can open any file in the module and understand it without a
22
22
  guided tour, and nothing sensitive leaks on the way. Rank every finding by "does fixing this
@@ -27,26 +27,26 @@ dev task) stay English.
27
27
 
28
28
  ---
29
29
 
30
- ## Phase 1Discover and pick the module
30
+ ## Phase 1 - Discover and pick the module
31
31
 
32
32
  1. Repo root via `git rev-parse --show-toplevel`.
33
- 2. **Discover module roots**any directory with a `Package.swift`, a `Sources/` subtree or an
33
+ 2. **Discover module roots** - any directory with a `Package.swift`, a `Sources/` subtree or an
34
34
  `.xcodeproj`. Sweep the container dirs that exist (`Domains/`, `Packages/`, `Modules/`,
35
35
  `Features/`, `Core/`, `Common/`, top-level module dirs) one level deep, then two if empty.
36
36
  3. Record per module: name · path · Swift files · lines · governance docs present · SPM targets.
37
- Keep this **module registry** for the whole run`MOD-*` greps every import against it.
37
+ Keep this **module registry** for the whole run - `MOD-*` greps every import against it.
38
38
  Classify each module's role, because the role decides which dependency edges are legal:
39
39
  **feature** · **core/common** · **seam** (cross-module contracts/bridges/navigation) ·
40
40
  **composition root** (legitimately knows every module).
41
41
  4. If `$ARGUMENTS` resolves to a module, skip the prompt. Otherwise `AskUserQuestion`
42
42
  (single-select, `outputLanguage`), documented modules first, each option showing file/line
43
43
  counts and `docs: yes/no`. If the list exceeds the question limit, group the smallest into an
44
- "other" option and ask againnever truncate silently.
44
+ "other" option and ask again - never truncate silently.
45
45
  5. Scope = the whole module's sources across all its targets, unless the user narrows it.
46
46
 
47
- ## Phase 2Resolve what applies to THIS module
47
+ ## Phase 2 - Resolve what applies to THIS module
48
48
 
49
- ### 2a. The registry is the standardin-module prose docs are NOT consulted
49
+ ### 2a. The registry is the standard - in-module prose docs are NOT consulted
50
50
 
51
51
  The skill's `references/rules.yml` (+ a project `modules/<Module>.yml` overlay) is the single source of truth. **Do not read, cite, or
52
52
  derive rules from a module's own `*-CLAUDE.md`, `docs/` set or any other in-repo prose.** They are
@@ -65,7 +65,7 @@ the specs the generator reads, and the module registry from Phase 1.
65
65
  be `blocking`.
66
66
  *Why prohibitions need to be written down:* inference reads dominant patterns, and a rule
67
67
  obeyed everywhere has no counter-example to infer from. A ban at 100% compliance is invisible
68
- to a scanit survives only if the overlay states it.
68
+ to a scan - it survives only if the overlay states it.
69
69
  2. **Repo-level rules** (root `CLAUDE.md`, contributing docs) apply to every module.
70
70
  3. **The module's own dominant pattern is its de-facto convention.** Count variants per dimension;
71
71
  the minority instances are the finding. Internal consistency beats conformity to a sibling.
@@ -75,14 +75,14 @@ Findings from step 3 are tagged `inferred` and capped at **suggestion**. Only re
75
75
  overlay-backed rules may be `blocking`.
76
76
 
77
77
  **When the overlay is missing**, generate a draft from code evidence, print it for confirmation,
78
- and mark every slot it could not derivea frozen UI target, a name lock, a carve-out and an
78
+ and mark every slot it could not derive - a frozen UI target, a name lock, a carve-out and an
79
79
  operational constraint all look like ordinary code from the outside. Do not guess them; list them
80
80
  as Open Questions.
81
81
 
82
82
  ### 2c. Vocabulary
83
83
 
84
84
  Bind each slot to a real symbol by grepping the module, then the reference module. **An unbound
85
- slot disables its rules**never invent a name, never import a sibling's.
85
+ slot disables its rules** - never invent a name, never import a sibling's.
86
86
 
87
87
  `HandlerName` · `EventParam` · `CoordinatorType` · `ResultType` / `ErrorType` / `ErrorFactory` ·
88
88
  `DIResolver` / `DIConfigurator` · `RequestModelSuffix` / `ResponseModelSuffix` / `TransportSuffix` ·
@@ -95,14 +95,14 @@ slot disables its rules** — never invent a name, never import a sibling's.
95
95
 
96
96
  Print the resolved table before scanning so a wrong binding is caught early.
97
97
 
98
- ### 2d. Sensitive-data inventoryrequired before any SEC rule runs
98
+ ### 2d. Sensitive-data inventory - required before any SEC rule runs
99
99
 
100
100
  The `SEC-*` rules are written against the **data classes** in `references/rules.yml →
101
101
  sensitive_data_classes`, never against one module's field names. Resolve the module's concrete
102
102
  instances of each class: grep entities, request/response models, storage calls and analytics
103
103
  events for the values the module actually handles, and map each to its class.
104
104
 
105
- Produce the inventory as a table**class · concrete symbols · where it enters · required
105
+ Produce the inventory as a table - **class · concrete symbols · where it enters · required
106
106
  lifetime (transient / survives-restart / survives-reinstall) · where it is actually stored ·
107
107
  where it is logged · where it leaves (network, analytics, pasteboard, another module)**.
108
108
 
@@ -118,13 +118,13 @@ whose required lifetime nobody can state, is an Open Question, not a silent pass
118
118
 
119
119
  This table is a deliverable in its own right, independent of whether any violation is found.
120
120
 
121
- ## Phase 3Scan
121
+ ## Phase 3 - Scan
122
122
 
123
123
  Apply the rules in `references/rules.yml` that survived Phase 2 binding, plus the module's own `validation`
124
124
  gate. Tag every finding: **rule ID · severity · source (`registry` / `doc` / `inferred`) ·
125
125
  onboarding impact (high/medium/low)**.
126
126
 
127
- ### Coverage gateon disk, not in memory
127
+ ### Coverage gate - on disk, not in memory
128
128
 
129
129
  Before scanning, write `.<module>-audit-coverage.tsv` to the scratch dir: one row per target
130
130
  (every screen under `ScreenRoot`, every shared component, every entity, every file in every
@@ -132,25 +132,25 @@ target) with columns `path · status · rules-applied`. Mark rows as you go.
132
132
 
133
133
  - Each target is **audited** or **explicitly skipped with a reason** (generated, fixture,
134
134
  vendored). No third state.
135
- - **The run is incomplete while any row is unmarked**and the file, not a claim, is the proof.
135
+ - **The run is incomplete while any row is unmarked** - and the file, not a claim, is the proof.
136
136
  Chunk the module if it is too large for one pass; the checklist survives a context reset.
137
137
  - No sampling. A partial audit reported as complete certifies the unscanned screens as clean.
138
138
 
139
139
  ### Grep hygiene
140
140
 
141
141
  Registry `mechanism` patterns are line-based and match doc comments. Filter
142
- `^[[:space:]]*//`BSD grep does not understand `\s`, so a `\s*//` filter silently lets `/// …`
143
- throughthen open each surviving hit and confirm it is real code. Report pre-filter and
142
+ `^[[:space:]]*//` - BSD grep does not understand `\s`, so a `\s*//` filter silently lets `/// ...`
143
+ through - then open each surviving hit and confirm it is real code. Report pre-filter and
144
144
  post-verification counts separately; a table padded with comment matches destroys trust.
145
145
 
146
146
  ### Judgement rules need evidence, not assertion
147
147
 
148
148
  For any rule marked `enforcement: judgement`, a finding must carry the measurement its `check`
149
- namesa reference count, a call-site count, a consumer count, a file list. **No count, no
149
+ names - a reference count, a call-site count, a consumer count, a file list. **No count, no
150
150
  finding.** This is what keeps a subjective rule from becoming an opinion.
151
151
 
152
152
  Every judgement rule has a worked ✗/✓ pair in `EXAMPLES.md`, keyed by ID. Cite it in the finding's
153
- proposed fix rather than re-describing the shapethe developer applying the fix should be
153
+ proposed fix rather than re-describing the shape - the developer applying the fix should be
154
154
  reading the same picture the audit used.
155
155
 
156
156
  ### Module-specific overrides
@@ -159,79 +159,79 @@ The module's own docs win over the registry, including where they contradict a s
159
159
  Never apply one module's dialect to another. List every override honoured, so the reader sees why
160
160
  a registry rule was not raised.
161
161
 
162
- ## Phase 4Produce the plan
162
+ ## Phase 4 - Produce the plan
163
163
 
164
164
  No findings → report compliant and stop. Otherwise write
165
165
  `<ModulePath>/ios-coding-standard-plan-<module>.md` (or the repo's scratch/docs dir if the module
166
166
  tree must stay clean), containing, in order:
167
167
 
168
- 1. **Coverage ledger**total targets · audited · skipped with reasons. Not 100% accounted for
168
+ 1. **Coverage ledger** - total targets · audited · skipped with reasons. Not 100% accounted for
169
169
  means the plan is not deliverable.
170
- 2. **Summary**counts by severity and source, resolved vocabulary, branch to create.
171
- 3. **Sensitive-data inventory** (Phase 2d)first, because it is the highest-risk content.
172
- 4. **Screen × role matrix** `[STRUCT-02]`screens as rows, manifest roles as columns, cells
170
+ 2. **Summary** - counts by severity and source, resolved vocabulary, branch to create.
171
+ 3. **Sensitive-data inventory** (Phase 2d) - first, because it is the highest-risk content.
172
+ 4. **Screen × role matrix** `[STRUCT-02]` - screens as rows, manifest roles as columns, cells
173
173
  `✓ / missing / misplaced`. The structural map a newcomer reads first.
174
- 5. **Type placement table** `[STRUCT-05]`type · location · consumer count · correct tier ·
174
+ 5. **Type placement table** `[STRUCT-05]` - type · location · consumer count · correct tier ·
175
175
  move required. Paired with the nested-type list `[STRUCT-01]`: owner · nested type · kind ·
176
176
  reference count · verdict.
177
- 6. **Visibility report** `[VIS-01, VIS-02]`over-exposed declarations per file, non-final
177
+ 6. **Visibility report** `[VIS-01, VIS-02]` - over-exposed declarations per file, non-final
178
178
  classes with no subclass.
179
- 7. **Boundary report** `[MOD-*]`import graph (allowed vs forbidden, with coupling depth) ·
179
+ 7. **Boundary report** `[MOD-*]` - import graph (allowed vs forbidden, with coupling depth) ·
180
180
  manifest graph (declared vs legal for the role, plus dead edges) · **removability delta**
181
- ("removing X touches N files: ") · inbound test (tests compile with no sibling present).
182
- 8. **Testability seam report** `[TEST-*]`types reaching for the environment, logic requiring a
181
+ ("removing X touches N files: ...") · inbound test (tests compile with no sibling present).
182
+ 8. **Testability seam report** `[TEST-*]` - types reaching for the environment, logic requiring a
183
183
  view to execute, doubles whose signatures have drifted.
184
- 9. **Change-cost probe** `[FLEX-*]`pick two plausible upcoming changes (a new variant of an
184
+ 9. **Change-cost probe** `[FLEX-*]` - pick two plausible upcoming changes (a new variant of an
185
185
  existing screen; a new field on a shared entity) and state what each costs in files today.
186
- 10. **Findings table**rule ID · `file:line` · current state · proposed fix · severity · source ·
186
+ 10. **Findings table** - rule ID · `file:line` · current state · proposed fix · severity · source ·
187
187
  onboarding impact.
188
- 11. **Per-file work**grouped per file, blocking → important → suggestion; within a severity,
188
+ 11. **Per-file work** - grouped per file, blocking → important → suggestion; within a severity,
189
189
  highest onboarding impact first.
190
- 12. **Behaviour-preservation guard**splitting files, adding MARKs, extracting extensions and
190
+ 12. **Behaviour-preservation guard** - splitting files, adding MARKs, extracting extensions and
191
191
  moving types must not change behaviour. Anything requiring a behaviour change (a missing
192
192
  analytics event, a wrong result type, a timezone fix, a storage migration) goes in a
193
193
  **separate group**, reviewed and tested on its own, never inside a mechanical split commit.
194
- 13. **Tooling delegation & the ratchet**the section that decides whether any of this survives.
194
+ 13. **Tooling delegation & the ratchet** - the section that decides whether any of this survives.
195
195
  - Classify each applied rule by its registry `enforcement`. For every `lint` / `format` /
196
196
  `scan` rule, state the concrete mechanism and whether that toolchain exists in the repo
197
197
  (`LintToolchain`). **When it does not exist, saying so is a finding**, ranked above most
198
198
  individual violations it would have caught.
199
199
  - Recommend **baseline-and-ratchet, never big-bang**: grandfather existing violations, surface
200
- only new ones, drain the backlog behind a green build. Order the plan the same waystop
200
+ only new ones, drain the backlog behind a green build. Order the plan the same way - stop
201
201
  the bleeding, then clean up.
202
202
  - Custom lint rule identifiers **are** the registry IDs (`sec_01_no_plaintext_persistence`,
203
203
  `mod_01_no_sibling_import`) so a violation points straight at the rule entry.
204
- - **Linting is local-only by decision**the skill's `references/lint-local.sh` with `references/swiftlint.draft.yml`, config
204
+ - **Linting is local-only by decision** - the skill's `references/lint-local.sh` with `references/swiftlint.draft.yml`, config
205
205
  and baseline kept outside the repository; no committed config, no build phase, no CI job.
206
206
  State this honestly in the plan rather than describing a gate that does not exist: a
207
207
  pre-PR habit catches less than a wall, so the residual risk is real and belongs in the
208
- report. Adopting it project-wide remains an open decisionrecord it as one so it stays
208
+ report. Adopting it project-wide remains an open decision - record it as one so it stays
209
209
  visible instead of quietly lapsing.
210
210
  - Never imply the audit substitutes for the linter, or the linter for a gate. The audit finds
211
211
  the debt once; the linter keeps a developer honest; only a gate stops it returning.
212
- 14. **Numeric exit criteria**target and today's measured value side by side: lint violations
212
+ 14. **Numeric exit criteria** - target and today's measured value side by side: lint violations
213
213
  per 1k lines (<1) · public-surface ratio, public declarations ÷ externally consumed symbols
214
214
  (≈1) · forbidden cross-module imports (0) · removability delta · files over threshold and
215
215
  files >120 lines with no sectioning (0) · screens missing a manifest role (0) · escape-hatch
216
216
  count `[CONC-03]` (flat or falling) · exception-marker count (flat or falling) · dead code (0)
217
217
  · screens with logic and no mirrored tests (0) · unclassified sensitive symbols (0).
218
- 15. **Exception ledger**every `// standard:exception(<ID>)` in the module: rule · reason ·
218
+ 15. **Exception ledger** - every `// standard:exception(<ID>)` in the module: rule · reason ·
219
219
  expiry · expired yes/no.
220
- 16. **Open Questions**Phase 2b dimensions with no majority, and unclassifiable symbols.
221
- 17. **Suggested commit slicing**one commit per file-group / concern.
222
- 18. **Scope guard**only this module is touched; definition of done is the module's own
220
+ 16. **Open Questions** - Phase 2b dimensions with no majority, and unclassifiable symbols.
221
+ 17. **Suggested commit slicing** - one commit per file-group / concern.
222
+ 18. **Scope guard** - only this module is touched; definition of done is the module's own
223
223
  verification path.
224
224
 
225
225
  Then write a **separate one-page onboarding summary** beside the plan, aimed at a developer
226
226
  joining next week rather than at whoever does the remediation: the ten always/never lines
227
227
  specialised to this module, the screen file manifest, the placement ladder, the sensitive-data
228
- classes in play, and the verification command. Nobody learns a codebase from a 40-finding audit
228
+ classes in play, and the verification command. Nobody learns a codebase from a 40-finding audit -
229
229
  this page is the actual deliverable, and the audit is how you earn the right to write it
230
- accurately. Derive it from `references/STANDARD.md § 0`, do not re-invent it.
230
+ accurately. Derive it from `references/STANDARD.md section 0`, do not re-invent it.
231
231
 
232
232
  Show a concise version of the plan to the user too.
233
233
 
234
- ## Phase 5Hand off
234
+ ## Phase 5 - Hand off
235
235
 
236
236
  `AskUserQuestion` (single-select, `outputLanguage`):
237
237
 
@@ -246,10 +246,10 @@ verifies, and commits only when asked. This routine ends at the handoff.
246
246
 
247
247
  ## Notes
248
248
 
249
- - **Verification reality is per-moduleestablish it, do not assume it.** Some targets cannot be
249
+ - **Verification reality is per-module - establish it, do not assume it.** Some targets cannot be
250
250
  compiled from the CLI (asset symbol generation, UIKit-dependent packages, a dependency failing
251
251
  earlier in the graph); there, verification is Xcode and a manifest dump validates the manifest
252
- only. Never trust a piped build's exit codea pipe reports the tail's status. Record the
252
+ only. Never trust a piped build's exit code - a pipe reports the tail's status. Record the
253
253
  module's actual verification command in the plan.
254
254
  - One module per run. Several modules means several runs and several plans, never one
255
255
  cross-module churn commit.
@@ -2,8 +2,8 @@
2
2
 
3
3
  The one document to read before your first PR. Rule IDs in brackets point at `rules.yml`, which
4
4
  carries severity, tooling and rationale for each; `EXAMPLES.md` carries a worked ✗/✓ pair for
5
- every rule a tool cannot decidelook one up by ID when a review cites it. Your module's own docs
6
- win where they differthis is the floor, not the ceiling.
5
+ every rule a tool cannot decide - look one up by ID when a review cites it. Your module's own docs
6
+ win where they differ - this is the floor, not the ceiling.
7
7
 
8
8
  Five principles, in the order they break things: **security · testability · readability ·
9
9
  flexibility · consistency.**
@@ -13,14 +13,14 @@ flexibility · consistency.**
13
13
  ## 0. The ten lines
14
14
 
15
15
  1. Sensitive data goes in the Keychain, never in `UserDefaults`, and never into a log. `[SEC-01, SEC-03]`
16
- 2. Never reach for the environmentinject time, storage, randomness, session. `[TEST-01]`
16
+ 2. Never reach for the environment - inject time, storage, randomness, session. `[TEST-01]`
17
17
  3. One type per file; the file is named after it. `[STRUCT-01]`
18
18
  4. A screen is a known set of files, always the same set. `[STRUCT-02]`
19
19
  5. Where a type lives is decided by how many things use it. `[STRUCT-05]`
20
20
  6. A feature module never imports another feature module. `[MOD-01]`
21
21
  7. Everything is `private` and `final` until something forces otherwise. `[VIS-01, VIS-02]`
22
- 8. One request in, one result out`async`, no completion handlers, no `throws`. `[SVC-01]`
23
- 9. Split concerns with `// MARK:`business rules, service calls and UI never share a section. `[READ-01]`
22
+ 8. One request in, one result out - `async`, no completion handlers, no `throws`. `[SVC-01]`
23
+ 9. Split concerns with `// MARK:` - business rules, service calls and UI never share a section. `[READ-01]`
24
24
  10. Variants are configuration, not `if` trees. `[FLEX-02]`
25
25
 
26
26
  ---
@@ -32,13 +32,13 @@ Work out which **data class** a value belongs to before you decide where it goes
32
32
  document, booking reference, membership identity, payment instrument, personal contact,
33
33
  biometric/health, precise location. The class decides the storage, not convenience.
34
34
 
35
- ### Storage `[SEC-01]`ask "does it persist?" before "where does it go?"
35
+ ### Storage `[SEC-01]` - ask "does it persist?" before "where does it go?"
36
36
 
37
37
  Keychain answers *where a persisted secret lives*. It does not answer *this value is sensitive*.
38
38
  Most sensitive values in a flow are used and dropped, and those must not be persisted at all.
39
39
 
40
40
  ```swift
41
- // ✗ over-persistencea value the flow throws away in 30 seconds now outlives logout
41
+ // ✗ over-persistence - a value the flow throws away in 30 seconds now outlives logout
42
42
  // and nobody owns deleting it
43
43
  try credentialStore.save(oneTimeCode, for: .verificationCode)
44
44
  UserDefaults.standard.set(passportNumber, forKey: "apisPassport")
@@ -50,7 +50,7 @@ UserDefaults.standard.set(passportNumber, forKey: "apisPassport")
50
50
  }
51
51
  ```
52
52
  ```swift
53
- // ✓ persisted, because it must survive an app restartKeychain, explicit accessibility,
53
+ // ✓ persisted, because it must survive an app restart - Keychain, explicit accessibility,
54
54
  // no iCloud sync
55
55
  try credentialStore.save(
56
56
  membershipToken,
@@ -61,15 +61,15 @@ try credentialStore.save(
61
61
 
62
62
  **The ladder, in order:**
63
63
 
64
- 1. **Does it need to outlive this flow?** Assume no. In-flow dataa form field, a scanned
65
- document number, a one-time code, a draftstays in memory and dies with the flow.
64
+ 1. **Does it need to outlive this flow?** Assume no. In-flow data - a form field, a scanned
65
+ document number, a one-time code, a draft - stays in memory and dies with the flow.
66
66
  2. **If yes: survive what?** Backgrounding → still just model state. App restart → Keychain.
67
67
  Reinstall → a product decision someone signs off, never a storage default.
68
68
  3. **Which class is it?** `payment-instrument`, `biometric-or-health` and `precise-location`
69
- stay transient no matter what step 2 saidthe answer to "it must survive" there is a
69
+ stay transient no matter what step 2 said - the answer to "it must survive" there is a
70
70
  server-side or system token, not local storage.
71
71
 
72
- **Why both directions are findings:** `UserDefaults` is a plist in the app containerit lands in
72
+ **Why both directions are findings:** `UserDefaults` is a plist in the app container - it lands in
73
73
  unencrypted backups and outlives the session, so under-protection is obvious. Over-persistence is
74
74
  the subtler one: an unnecessary Keychain item survives the flow, survives logout unless someone
75
75
  remembers to delete it, and creates a cleanup obligation with no owner. "Put it in the Keychain to
@@ -95,11 +95,11 @@ private means a forgotten annotation fails safe; defaulting to public means it f
95
95
 
96
96
  ### The rest, briefly
97
97
 
98
- - No secret in sourceanything committed is already leaked. `[SEC-02]`
98
+ - No secret in source - anything committed is already leaked. `[SEC-02]`
99
99
  - HTTPS only; an ATS exception carries a written reason and an expiry date. `[SEC-04]`
100
100
  - Sensitive data has a lifetime: cleared on logout, hidden from the app-switcher snapshot,
101
101
  not cached to disk by default. `[SEC-05]`
102
- - Analytics events, user properties and crash breadcrumbs are redactedcheck the parameter
102
+ - Analytics events, user properties and crash breadcrumbs are redacted - check the parameter
103
103
  list of every event you add. `[SEC-06]`
104
104
  - Permissions are least-privilege with honest purpose strings; the privacy manifest matches what
105
105
  you actually collect. `[SEC-07, SEC-08]`
@@ -115,7 +115,7 @@ Testability is a property of the production code. You cannot add it later by wri
115
115
  ### Inject the environment `[TEST-01]`
116
116
 
117
117
  ```swift
118
- // ✗ untestable by constructionthe assertion depends on today's date
118
+ // ✗ untestable by construction - the assertion depends on today's date
119
119
  struct BoardingEligibility {
120
120
  func canCheckIn(flight: Flight) -> Bool {
121
121
  Date() > flight.departure.addingTimeInterval(-24 * 3600)
@@ -132,7 +132,7 @@ struct BoardingEligibility {
132
132
  }
133
133
  }
134
134
  ```
135
- **Why:** the rule is not "avoid `Date()`"it is that anything the outside world decides
135
+ **Why:** the rule is not "avoid `Date()`" - it is that anything the outside world decides
136
136
  (time, randomness, identifiers, locale, storage, session, feature flags) must be something the
137
137
  test can decide instead. A type that reaches for it has no seam, and no test discipline recovers.
138
138
 
@@ -157,7 +157,7 @@ func submitGate(for passengers: [Passenger], isLoggedIn: Bool) -> SubmitGate {
157
157
  needs a session, a network stub and a view model instance to answer "what happens when a document
158
158
  is missing".
159
159
 
160
- Also: name test doubles for what they dostub, spy, fake, mockone kind per file, and keep
160
+ Also: name test doubles for what they do - stub, spy, fake, mock - one kind per file, and keep
161
161
  the signature identical to the real type, because a drifted double is the first thing the next
162
162
  person copies. `[TEST-04, SVC-02]`
163
163
 
@@ -176,7 +176,7 @@ Error handling`. Scene order: `State → Init → Body →` one `@ViewBuilder` p
176
176
  Two or more call sites → its own file with its own configuration. Exactly one call site and bound
177
177
  to the screen's state → a `private @ViewBuilder` in a MARK'd extension. Pushing a state-coupled
178
178
  fragment into its own file to shrink the screen trades one long file for a file plus a binding
179
- tanglethat reads worse, and it is a finding in the same way the opposite is.
179
+ tangle - that reads worse, and it is a finding in the same way the opposite is.
180
180
 
181
181
  ### A pure transform is a shared helper `[READ-04d]`
182
182
 
@@ -193,7 +193,7 @@ enum OrderBFFFormatters {
193
193
  static func initials(from name: String?) -> String? { ... }
194
194
  }
195
195
  ```
196
- **Why:** these have no screen state, so nothing ties them to a screenand left where they were
196
+ **Why:** these have no screen state, so nothing ties them to a screen - and left where they were
197
197
  typed they get written a second time somewhere else, with a slightly different edge case.
198
198
 
199
199
  ### A view fragment that renders a thing is a component file `[READ-04b]`
@@ -210,7 +210,7 @@ private var legSwitcher: some View {
210
210
  }
211
211
  ```
212
212
  ```swift
213
- // ✓ SeatMapLegSwitcher.swiftdata in, callbacks out, previewable on its own
213
+ // ✓ SeatMapLegSwitcher.swift - data in, callbacks out, previewable on its own
214
214
  struct SeatMapLegSwitcher: View {
215
215
  let segments: [SeatMapSegment]
216
216
  let activeSegmentIndex: Int
@@ -222,20 +222,20 @@ struct SeatMapLegSwitcher: View {
222
222
  SeatMapLegSwitcher(segments: [.gidis, .donus], activeSegmentIndex: 0, onSelect: { _ in })
223
223
  }
224
224
 
225
- // ✓ the scene keeps the compositionwhich component shows, in what order
225
+ // ✓ the scene keeps the composition - which component shows, in what order
226
226
  @ViewBuilder
227
227
  var content: some View {
228
228
  if viewModel.loadError { errorRetryView } else { SeatMapLegSwitcher(...) }
229
229
  }
230
230
  ```
231
231
  **Why:** the canvas is the fastest way to check a visual piece, and it only works when the piece
232
- takes data. A fragment bound to a view model needs the DI container no preview configuresso
232
+ takes data. A fragment bound to a view model needs the DI container no preview configures - so
233
233
  it never gets looked at until the whole flow is run on a device.
234
234
 
235
235
  ### One type per file; nest only owned details `[STRUCT-01]`
236
236
 
237
237
  ```swift
238
- // ✗ a response model nested inside anotherinvisible to a filename search,
238
+ // ✗ a response model nested inside another - invisible to a filename search,
239
239
  // and moving it later renames every reference
240
240
  struct OrderResponseModel {
241
241
  struct PassengerModel { ... }
@@ -246,12 +246,12 @@ struct OrderResponseModel {
246
246
  struct OrderResponseModel { let items: [OrderItemModel] } // OrderResponseModel.swift
247
247
  struct PassengerModel { ... } // PassengerModel.swift
248
248
 
249
- // ✓ still finean owned detail with exactly one owner
249
+ // ✓ still fine - an owned detail with exactly one owner
250
250
  @Observable final class SeatMapViewModel {
251
251
  enum ViewState { case loading, loaded, failed }
252
252
  }
253
253
 
254
- // ✓ also finea pure constants namespace; the nesting IS the grouping
254
+ // ✓ also fine - a pure constants namespace; the nesting IS the grouping
255
255
  enum AppConstant {
256
256
  enum Phone { static let defaultDialCode = "+90" }
257
257
  enum DeepLink { static let scheme = "myapp" }
@@ -259,13 +259,13 @@ enum AppConstant {
259
259
  ```
260
260
  **Why:** entities and transport models get looked up by name, move between placement tiers as
261
261
  consumers change, and are referenced from mappers and tests. A `ViewState` or a `static let`
262
- literal does none of thatflattening `AppConstant.Phone` to `AppConstantPhone` loses the
262
+ literal does none of that - flattening `AppConstant.Phone` to `AppConstantPhone` loses the
263
263
  grouping and buys no discoverability.
264
264
 
265
265
  ### A method that wraps one service is named after it `[SVC-07]`
266
266
 
267
267
  ```swift
268
- // ✗ transport verbs invented by the clientthe name says what the code does
268
+ // ✗ transport verbs invented by the client - the name says what the code does
269
269
  // (which the signature already says), not which service will fire
270
270
  func fetchOpenStatus(_ request: CheckOpenStatusRequestModel) async -> Result
271
271
  func loadPassengers(...) async -> Result
@@ -317,7 +317,7 @@ var variant: SummaryVariant {
317
317
  **Why:** the mapper is the one type with no screen context. A rule buried in it is invisible
318
318
  from the view model that owns the behaviour, untestable without hand-building a DTO, and quietly
319
319
  duplicated the next time another screen needs the same decision. `?? ""` on an optional wire
320
- field is not a decisionit is the lowering itself.
320
+ field is not a decision - it is the lowering itself.
321
321
 
322
322
  ### Signatures read as the contract `[SVC-01]`
323
323
 
@@ -339,7 +339,7 @@ result family means one error channel instead of `throws` plus a result plus an
339
339
  ### Visibility is documentation `[VIS-01, VIS-02, VIS-04]`
340
340
 
341
341
  Everything `private` and `final` until something forces otherwise. `public` only on what another
342
- module actually importsand every surviving `public`, plus every cross-module contract, carries
342
+ module actually imports - and every surviving `public`, plus every cross-module contract, carries
343
343
  a doc comment. That is the one place the "no unnecessary comments" rule inverts: an
344
344
  implementation detail explains itself through naming, a contract between two teams cannot.
345
345
 
@@ -350,7 +350,7 @@ implementation detail explains itself through naming, a contract between two tea
350
350
  ### A screen is a known file manifest `[STRUCT-02, STRUCT-03]`
351
351
 
352
352
  `Scene · ViewModel · LocalizedText · CoordinatorEvent · AnalyticsTracking · UseCase · Repository
353
- (+protocol +mock) · Mapper + models`each present when its responsibility exists, absent when it
353
+ (+protocol +mock) · Mapper + models` - each present when its responsibility exists, absent when it
354
354
  does not. An empty `LocalizedText` on a screen with no copy is noise, not compliance. Every screen
355
355
  sits at the same depth with the same internal grouping, because people navigate by muscle memory.
356
356
 
@@ -360,7 +360,7 @@ sits at the same depth with the same internal grouping, because people navigate
360
360
  |---|---|
361
361
  | 2+ modules | cross-module shared tier |
362
362
  | 2+ screens | the module's shared entities |
363
- | one screen | that screen's own folder**not** the shared tier |
363
+ | one screen | that screen's own folder - **not** the shared tier |
364
364
  | one type | its own file beside that type |
365
365
 
366
366
  Both directions are findings. A single-consumer type parked in the shared tier inflates the shared
@@ -369,7 +369,7 @@ surface and makes the next reader think it is load-bearing.
369
369
  ### Modules do not know each other `[MOD-01, MOD-05, MOD-06]`
370
370
 
371
371
  A feature module never imports a sibling feature. Cross-feature needs go through the seam layer
372
- (contracts / bridges / adapters). DI resolves a protocol declared in core or the seamresolving
372
+ (contracts / bridges / adapters). DI resolves a protocol declared in core or the seam - resolving
373
373
  another feature's concrete type is a compile-time dependency in a runtime disguise. Only the
374
374
  composition root knows the module list.
375
375
 
@@ -390,7 +390,7 @@ Text(title)
390
390
  .padding(style.padding)
391
391
  ```
392
392
  **Why:** adding a third variant costs a case instead of a branch, and the component stays open for
393
- extensionthe next design change does not edit its body.
393
+ extension - the next design change does not edit its body.
394
394
 
395
395
  ---
396
396
 
@@ -403,7 +403,7 @@ cases, so these rules are about the model being *legible*.
403
403
  runs from its declaration, without tracing callers. `[CONC-01]`
404
404
  - State `Sendable` where it is load-bearing, consistently. `[CONC-02]`
405
405
  - `@preconcurrency`, `nonisolated(unsafe)` and `@unchecked Sendable` are migration tools. Each
406
- needs a reason and a removal conditionthey are counted, and a rising count means the module
406
+ needs a reason and a removal condition - they are counted, and a rising count means the module
407
407
  is quietly returning to pre-Swift-6 guarantees while the build stays green. `[CONC-03]`
408
408
  - One model: no `DispatchQueue`, semaphore or completion handler layered onto `async`. `[CONC-04]`
409
409
  - Every task has an owner and a cancellation story. An unowned task outlives its screen and writes
@@ -416,19 +416,19 @@ cases, so these rules are about the model being *legible*.
416
416
  Identifier from the shared source on every interactive element `[A11Y-01]` · localized VoiceOver
417
417
  label, plus a hint when the action is not obvious from the label `[A11Y-02]` · 44×44 minimum tap
418
418
  target, and grouped content exposes one meaningful element rather than five fragments `[A11Y-03]`
419
- · Dynamic Type survives the largest accessibility sizesno fixed-height container around
419
+ · Dynamic Type survives the largest accessibility sizes - no fixed-height container around
420
420
  scalable text `[A11Y-04]` · RTL mirrors, so `leading`/`trailing`, never `left`/`right` `[A11Y-05]`.
421
421
 
422
422
  ---
423
423
 
424
- ## 7. Performancethe four that are also readability
424
+ ## 7. Performance - the four that are also readability
425
425
 
426
426
  No expensive computation in a view body `[PERF-01]` · lazy containers with stable identity, never
427
427
  index-as-id `[PERF-02]` · no blocking work at init or on the main actor `[PERF-03]` · no formatter,
428
428
  calendar or regex constructed per render `[PERF-04]`.
429
429
 
430
430
  **Explicitly out of scope of this standard:** Instruments-driven optimisation, launch-time budgets,
431
- memory profiling. Those belong to a performance workflowmeasure before optimising, and do not
431
+ memory profiling. Those belong to a performance workflow - measure before optimising, and do not
432
432
  let a style document push you into speculative tuning.
433
433
 
434
434
  ---
@@ -44,7 +44,7 @@ if ! command -v swiftlint >/dev/null 2>&1; then
44
44
  cat >&2 <<'EOF'
45
45
  swiftlint is not installed.
46
46
 
47
- Install it for your user onlythis does not touch the project:
47
+ Install it for your user only - this does not touch the project:
48
48
 
49
49
  brew install swiftlint
50
50
 
@@ -82,7 +82,7 @@ fi
82
82
  : "${SENSITIVE:=token|password|passcode|secret|credential|passport|nationalId|identityNumber|pnr|reservationCode|ticketNumber|membershipNumber|cardNumber|cvv|dateOfBirth}"
83
83
  : "${TRANSIENT:=oneTimeCode|otp|verificationCode|draft}"
84
84
 
85
- # SIBLINGS: every other module that lives beside this onethe MOD-01 import guard.
85
+ # SIBLINGS: every other module that lives beside this one - the MOD-01 import guard.
86
86
  if [ -z "${SIBLINGS:-}" ]; then
87
87
  PARENT="$(dirname "$MODULE_ABS")"
88
88
  SIBLINGS="$(find "$PARENT" -mindepth 1 -maxdepth 1 -type d ! -name "$MODULE_NAME" -exec basename {} \; \
@@ -106,7 +106,7 @@ text = open(src).read()
106
106
  for key in ("SENSITIVE", "TRANSIENT", "SIBLINGS"):
107
107
  # Wrapped in a non-capturing group: these values are alternations, and "|" binds looser than
108
108
  # concatenation. Substituting them bare turns "prefix\bA|B|C" into "(prefix\bA) or (B) or (C)",
109
- # which matches every line containing any bare termthe whole pattern silently collapses.
109
+ # which matches every line containing any bare term - the whole pattern silently collapses.
110
110
  text = text.replace("((%s))" % key, "(?:%s)" % os.environ[key])
111
111
  text = text.replace("((LOGIC))", os.environ["LOGIC"])
112
112
  open(dst, "w").write(text)
@@ -118,7 +118,7 @@ echo "module : $MODULE_ABS"
118
118
  echo "config : $RESOLVED"
119
119
  echo "workdir : $MODULE_WORK (outside the repo)"
120
120
  echo "siblings : $SIBLINGS"
121
- [ -f "$INVENTORY" ] && echo "inventory : $INVENTORY" || echo "inventory : (noneusing the generic fallback pattern)"
121
+ [ -f "$INVENTORY" ] && echo "inventory : $INVENTORY" || echo "inventory : (none - using the generic fallback pattern)"
122
122
  echo
123
123
 
124
124
  cd "$MODULE_ABS"
@@ -143,7 +143,7 @@ ARGS=(lint --config "$RESOLVED" --reporter "$REPORTER" --quiet)
143
143
  [ -n "$STRICT" ] && ARGS+=("$STRICT")
144
144
 
145
145
  set +e
146
- # stdout is the report, stderr is progress + config diagnosticsnever merge them, or a
146
+ # stdout is the report, stderr is progress + config diagnostics - never merge them, or a
147
147
  # machine-readable reporter comes back with progress lines spliced into it.
148
148
  swiftlint "${ARGS[@]}" "${LINT_PATHS[@]}" 2>"$MODULE_WORK/last-run.err" | tee "$MODULE_WORK/last-run.txt"
149
149
  STATUS=${PIPESTATUS[0]}