@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.
- package/CHANGELOG.md +50 -0
- package/install/_plugin-skills.mjs +246 -0
- package/install/codex.mjs +46 -6
- package/install/copilot.mjs +28 -7
- package/install/templates/codex-instructions.md +23 -0
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +50 -50
- package/pipeline/multi-agent-refs/cross-cli-contract.md +32 -1
- package/pipeline/scripts/build-stack-plugins.mjs +50 -4
- package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +50 -50
- package/pipeline/skills/shared/external/ios-coding-standard/references/STANDARD.md +37 -37
- package/pipeline/skills/shared/external/ios-coding-standard/references/lint-local.sh +5 -5
- package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +108 -108
- package/pipeline/skills/shared/external/ios-coding-standard/references/swiftlint.draft.yml +25 -25
|
@@ -6,17 +6,17 @@ user-invocable: true
|
|
|
6
6
|
argument-hint: "[module name or path]"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
# multi-agent ios-coding-standard
|
|
9
|
+
# multi-agent ios-coding-standard - Module audit → plan → dev handoff
|
|
10
10
|
|
|
11
|
-
**Input**: $ARGUMENTS
|
|
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 here
|
|
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 source
|
|
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 1
|
|
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**
|
|
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
|
|
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 again
|
|
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 2
|
|
47
|
+
## Phase 2 - Resolve what applies to THIS module
|
|
48
48
|
|
|
49
|
-
### 2a. The registry is the standard
|
|
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 scan
|
|
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 derive
|
|
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**
|
|
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 inventory
|
|
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
|
|
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 3
|
|
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 gate
|
|
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**
|
|
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:]]*//`
|
|
143
|
-
through
|
|
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
|
-
names
|
|
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 shape
|
|
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 4
|
|
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**
|
|
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**
|
|
171
|
-
3. **Sensitive-data inventory** (Phase 2d)
|
|
172
|
-
4. **Screen × role matrix** `[STRUCT-02]`
|
|
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]`
|
|
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]`
|
|
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-*]`
|
|
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:
|
|
182
|
-
8. **Testability seam report** `[TEST-*]`
|
|
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-*]`
|
|
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**
|
|
186
|
+
10. **Findings table** - rule ID · `file:line` · current state · proposed fix · severity · source ·
|
|
187
187
|
onboarding impact.
|
|
188
|
-
11. **Per-file work**
|
|
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**
|
|
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**
|
|
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 way
|
|
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**
|
|
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 decision
|
|
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**
|
|
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**
|
|
218
|
+
15. **Exception ledger** - every `// standard:exception(<ID>)` in the module: rule · reason ·
|
|
219
219
|
expiry · expired yes/no.
|
|
220
|
-
16. **Open Questions**
|
|
221
|
-
17. **Suggested commit slicing**
|
|
222
|
-
18. **Scope guard**
|
|
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
|
|
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 5
|
|
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-module
|
|
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 code
|
|
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 decide
|
|
6
|
-
win where they differ
|
|
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 environment
|
|
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
|
|
23
|
-
9. Split concerns with `// MARK:`
|
|
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]`
|
|
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-persistence
|
|
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 restart
|
|
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 data
|
|
65
|
-
document number, a one-time code, a draft
|
|
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 said
|
|
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 container
|
|
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 source
|
|
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 redacted
|
|
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 construction
|
|
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()`"
|
|
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 do
|
|
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
|
-
tangle
|
|
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 screen
|
|
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.swift
|
|
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 composition
|
|
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 configures
|
|
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 another
|
|
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 fine
|
|
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 fine
|
|
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 that
|
|
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 client
|
|
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 decision
|
|
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 imports
|
|
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`
|
|
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
|
|
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 seam
|
|
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
|
-
extension
|
|
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 condition
|
|
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 sizes
|
|
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. Performance
|
|
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 workflow
|
|
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 only
|
|
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 one
|
|
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 term
|
|
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 : (none
|
|
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 diagnostics
|
|
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]}
|