@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
|
@@ -3,7 +3,7 @@ updated: 2026-07-26
|
|
|
3
3
|
owner: iOS platform
|
|
4
4
|
description: >
|
|
5
5
|
The rule registry. STANDARD.md teaches these to a newcomer with examples; SKILL.md audits
|
|
6
|
-
against them. IDs are stable and never renumbered
|
|
6
|
+
against them. IDs are stable and never renumbered - a rule is retired by status, not deletion.
|
|
7
7
|
severity_levels: [blocking, important, suggestion]
|
|
8
8
|
enforcement_kinds:
|
|
9
9
|
format: the formatter owns it; not reviewed by humans
|
|
@@ -20,7 +20,7 @@ persistence_decision:
|
|
|
20
20
|
ladder:
|
|
21
21
|
- step: 1
|
|
22
22
|
question: Does this value need to outlive the current flow?
|
|
23
|
-
default: "No
|
|
23
|
+
default: "No - assume transient until a requirement says otherwise"
|
|
24
24
|
if_no: >
|
|
25
25
|
Keep it in memory for the duration of the flow and drop it when the flow ends. Do NOT
|
|
26
26
|
write it to the Keychain: an unnecessary keychain item survives the flow, survives logout
|
|
@@ -28,7 +28,7 @@ persistence_decision:
|
|
|
28
28
|
Over-persisting is itself a finding, not a safe default.
|
|
29
29
|
if_yes: go to step 2
|
|
30
30
|
- step: 2
|
|
31
|
-
question: What does it need to survive
|
|
31
|
+
question: What does it need to survive - app backgrounding, app restart, or reinstall?
|
|
32
32
|
guidance: >
|
|
33
33
|
Backgrounding only -> in-memory state owned by the flow's model is still correct.
|
|
34
34
|
App restart -> Keychain with the accessibility class the data demands.
|
|
@@ -37,7 +37,7 @@ persistence_decision:
|
|
|
37
37
|
question: Which data class is it?
|
|
38
38
|
guidance: >
|
|
39
39
|
The class (below) decides the storage tier and the logging rule. never-persist-locally
|
|
40
|
-
classes stay transient no matter what step 2 said
|
|
40
|
+
classes stay transient no matter what step 2 said - the answer to "it needs to survive"
|
|
41
41
|
for a payment instrument or a biometric is a server-side or system-provided token, not
|
|
42
42
|
local storage.
|
|
43
43
|
transient_obligations: >
|
|
@@ -51,7 +51,7 @@ sensitive_data_classes:
|
|
|
51
51
|
Each audited module resolves its own concrete instances into a sensitive-data inventory
|
|
52
52
|
(SKILL.md Phase 2d), and the lint mechanisms are generated from that inventory rather than
|
|
53
53
|
hardcoded. A class with no instance in the module simply yields no rule.
|
|
54
|
-
The at_rest value below applies ONLY to values that passed the persistence ladder
|
|
54
|
+
The at_rest value below applies ONLY to values that passed the persistence ladder - it says
|
|
55
55
|
where a value goes IF it must persist, never that it must.
|
|
56
56
|
classes:
|
|
57
57
|
- id: auth-token
|
|
@@ -100,9 +100,9 @@ sensitive_data_classes:
|
|
|
100
100
|
|
|
101
101
|
rules:
|
|
102
102
|
|
|
103
|
-
# ── READ
|
|
103
|
+
# ── READ - format & readability ───────────────────────────────────────────
|
|
104
104
|
- id: READ-01
|
|
105
|
-
title: MARK sections separate concerns
|
|
105
|
+
title: MARK sections separate concerns - business rules, service calls, UI, one group each
|
|
106
106
|
severity: important
|
|
107
107
|
enforcement: judgement
|
|
108
108
|
rationale: readability
|
|
@@ -110,7 +110,7 @@ rules:
|
|
|
110
110
|
A file mixing a service call, a business rule and a view fragment under one MARK (or none)
|
|
111
111
|
is a finding even when short. Section order follows the module's conventions doc.
|
|
112
112
|
In a use case or repository implementation the sections are named after WHAT EACH TALKS TO
|
|
113
|
-
|
|
113
|
+
- `Service requests`, `On-device cache`, and so on - so a reader sees at a glance which
|
|
114
114
|
calls leave the device and which do not.
|
|
115
115
|
|
|
116
116
|
- id: READ-02
|
|
@@ -121,16 +121,16 @@ rules:
|
|
|
121
121
|
rationale: readability
|
|
122
122
|
check: >
|
|
123
123
|
Trim a comment by deleting what the code already says, the design-frame archaeology and the
|
|
124
|
-
historical aside
|
|
124
|
+
historical aside - never the decision. If a sentence records WHY a shape was chosen, or what
|
|
125
125
|
breaks without it, it stays however long the block ends up.
|
|
126
126
|
|
|
127
|
-
Carve-out
|
|
127
|
+
Carve-out - a comment that IS data is not prose and is not trimmed: a trigger/prefix table,
|
|
128
128
|
a state-transition matrix, a wire-contract enumeration. Those exist nowhere else in the
|
|
129
129
|
codebase, so shortening them destroys the only copy. Judge a long block by whether it
|
|
130
130
|
repeats the code or replaces a missing document.
|
|
131
131
|
|
|
132
132
|
- id: READ-03
|
|
133
|
-
title: Size thresholds with carve-outs
|
|
133
|
+
title: Size thresholds with carve-outs - screen 600 / file 400 / function 40 lines
|
|
134
134
|
severity: important
|
|
135
135
|
enforcement: lint
|
|
136
136
|
mechanism: swiftlint file_length, function_body_length, type_body_length
|
|
@@ -138,9 +138,9 @@ rules:
|
|
|
138
138
|
exempt: [generated sources, mock/fixture data files]
|
|
139
139
|
tooling_limitation: >
|
|
140
140
|
SwiftLint's built-in length rules take no per-rule `excluded`, so the mock/fixture carve-out
|
|
141
|
-
cannot be expressed in the config
|
|
141
|
+
cannot be expressed in the config - those files still report. Honour the exemption in the
|
|
142
142
|
audit pass and in the baseline; do not "fix" a fixture file to satisfy a line count.
|
|
143
|
-
check: File over 120 lines with zero MARK is a separate finding
|
|
143
|
+
check: File over 120 lines with zero MARK is a separate finding - a reader has no map.
|
|
144
144
|
|
|
145
145
|
- id: READ-04
|
|
146
146
|
title: Component placement is decided by call-site count, not by feel
|
|
@@ -158,7 +158,7 @@ rules:
|
|
|
158
158
|
enforcement: judgement
|
|
159
159
|
rationale: readability
|
|
160
160
|
check: >
|
|
161
|
-
A bar that reads as part of the screen's header
|
|
161
|
+
A bar that reads as part of the screen's header - a route chip, a countdown, a step bar -
|
|
162
162
|
goes inside the header component's add-on slot, not as the first row of the scroll
|
|
163
163
|
content. Rendered below the header it squares off the header's rounded bottom corners and
|
|
164
164
|
scrolls away from the title it belongs to. The header component has the slot; use it.
|
|
@@ -170,19 +170,19 @@ rules:
|
|
|
170
170
|
rationale: testability
|
|
171
171
|
check: >
|
|
172
172
|
A `private var x: some View` inside a scene is a component in disguise. If it renders a
|
|
173
|
-
THING
|
|
173
|
+
THING - a switcher, a chip row, a banner, a bar, a legend, a card - extract it to its own
|
|
174
174
|
file under the screen's `Presentation/Components/`, as a `struct` that takes DATA and
|
|
175
175
|
CALLBACKS, never the view model, and give it a `#Preview`.
|
|
176
176
|
Taking data instead of the view model is what makes the preview possible at all: a
|
|
177
177
|
component holding a view model needs the DI container a canvas preview never configures,
|
|
178
178
|
and the workaround (a preview-only fixture type shipped in production sources) is itself
|
|
179
179
|
a finding.
|
|
180
|
-
What stays a `@ViewBuilder` on the scene: the composition itself
|
|
180
|
+
What stays a `@ViewBuilder` on the scene: the composition itself - the piece that orders
|
|
181
181
|
the components, branches on loaded / error / empty, and reads the view model to decide
|
|
182
182
|
WHICH component shows. That is business-rule display and it belongs with the screen.
|
|
183
183
|
The test is what the fragment DOES, not how long it is. A row of icon + two labels + a
|
|
184
184
|
button is a component even at ten lines; a three-line `if loaded { a } else { b }` is
|
|
185
|
-
composition. A fragment that renders a thing AND takes no parameters is still a component
|
|
185
|
+
composition. A fragment that renders a thing AND takes no parameters is still a component -
|
|
186
186
|
"it needs no arguments" means the data is hardcoded or read off the view model, and both
|
|
187
187
|
are reasons to extract, not to keep it.
|
|
188
188
|
A component the design system already owns (the shared UI package) needs no wrapper and
|
|
@@ -195,7 +195,7 @@ rules:
|
|
|
195
195
|
rationale: testability
|
|
196
196
|
check: >
|
|
197
197
|
A `private var x: Bool/String/Int` in a scene that reads the view model and decides
|
|
198
|
-
something
|
|
198
|
+
something - "is the header shown", "which leg is active", "what does the CTA say" - is a
|
|
199
199
|
business rule in the view layer. It belongs on the view model, in its business-rules
|
|
200
200
|
section, where a unit test can reach it. The scene reads `viewModel.x`.
|
|
201
201
|
Not this rule: building a design-system `Configuration` value. Moving those to the view
|
|
@@ -208,8 +208,8 @@ rules:
|
|
|
208
208
|
enforcement: judgement
|
|
209
209
|
rationale: flexibility
|
|
210
210
|
check: >
|
|
211
|
-
A function in a scene that touches no view-model state and returns no view
|
|
212
|
-
format, a duration split, initials from a name, a unit conversion
|
|
211
|
+
A function in a scene that touches no view-model state and returns no view - a date
|
|
212
|
+
format, a duration split, initials from a name, a unit conversion - is a value transform.
|
|
213
213
|
It belongs in the module's formatter / extension home (`Common/Formatters`, a typed
|
|
214
214
|
`X+Extension`), not on whichever screen needed it first.
|
|
215
215
|
The tell is duplication: the same `initials(_:)` written once in a scene and again in a
|
|
@@ -218,19 +218,19 @@ rules:
|
|
|
218
218
|
**A domain-local formatter type is the second choice, not the first.** Search the shared
|
|
219
219
|
layer BEFORE writing one: a date/number/string transform with no domain vocabulary in it
|
|
220
220
|
is not a domain concern, and the app already has a home for it (`CoreExtensions`,
|
|
221
|
-
`CoreFormatters`). Grep the behaviour, not the name
|
|
221
|
+
`CoreFormatters`). Grep the behaviour, not the name - the existing helper will be spelled
|
|
222
222
|
differently (`Date.localizedBFFDate` vs a hand-rolled `displayDate`), and a per-domain
|
|
223
223
|
`XFormatters` full of re-implementations of it is how three domains end up with three
|
|
224
224
|
subtly different renderings of one contract field. When the shared helper is 90% right,
|
|
225
225
|
extend IT (a new `Format` case, a defaulted parameter) rather than forking it.
|
|
226
226
|
A domain-local formatter type is correct only when it adds domain vocabulary on top of the
|
|
227
|
-
shared primitives
|
|
227
|
+
shared primitives - naming which two formats a screen's pickers exchange, for instance -
|
|
228
228
|
and implements no formatting of its own.
|
|
229
229
|
Not this rule: a function that builds a design-system `Configuration` from screen data.
|
|
230
230
|
That is the component's own lowering and belongs inside the component (READ-04b).
|
|
231
231
|
|
|
232
232
|
- id: READ-05
|
|
233
|
-
title: Multi-line signature style
|
|
233
|
+
title: Multi-line signature style - open paren at line end, one parameter per line
|
|
234
234
|
severity: suggestion
|
|
235
235
|
enforcement: format
|
|
236
236
|
mechanism: swiftformat wrapArguments
|
|
@@ -250,14 +250,14 @@ rules:
|
|
|
250
250
|
rationale: readability
|
|
251
251
|
|
|
252
252
|
- id: READ-08
|
|
253
|
-
title: Forbidden constructs
|
|
253
|
+
title: Forbidden constructs - force unwrap, force cast, IUO, magic numbers, raw colors/fonts, print
|
|
254
254
|
severity: blocking
|
|
255
255
|
enforcement: lint
|
|
256
256
|
mechanism: swiftlint force_unwrapping, force_cast, force_try, implicitly_unwrapped_optional; custom no_print, no_raw_hex, no_raw_font
|
|
257
257
|
rationale: security
|
|
258
258
|
|
|
259
259
|
- id: READ-09
|
|
260
|
-
title: Standard file header
|
|
260
|
+
title: Standard file header - own target module, and the author's FULL git identity
|
|
261
261
|
check: >
|
|
262
262
|
The author line carries the identity exactly as git records it, department included
|
|
263
263
|
("NAME - <Department> Mudurlugu"). A bare name does not say which team owns the file, and
|
|
@@ -267,7 +267,7 @@ rules:
|
|
|
267
267
|
mechanism: swiftlint file_header
|
|
268
268
|
rationale: readability
|
|
269
269
|
|
|
270
|
-
# ── STRUCT
|
|
270
|
+
# ── STRUCT - project structure ────────────────────────────────────────────
|
|
271
271
|
- id: STRUCT-01
|
|
272
272
|
title: One primary top-level type per file; nesting only for owned details
|
|
273
273
|
severity: important
|
|
@@ -276,10 +276,10 @@ rules:
|
|
|
276
276
|
check: >
|
|
277
277
|
Forbidden nested: entity, domain model, request/response payload, list element. Allowed
|
|
278
278
|
nested: a configuration/style/state helper with exactly one owner and no second reference
|
|
279
|
-
site; a nested type a MACRO or an external design mapping owns
|
|
280
|
-
`X.Section`, a Code Connect-mapped `X.State`
|
|
279
|
+
site; a nested type a MACRO or an external design mapping owns - a `@FormSection`-generated
|
|
280
|
+
`X.Section`, a Code Connect-mapped `X.State` - where flattening silently renames a
|
|
281
281
|
contract the generator or the design file still refers to (the compiler catches the macro
|
|
282
|
-
case, the design mapping fails silently); and a pure constants namespace
|
|
282
|
+
case, the design mapping fails silently); and a pure constants namespace - a caseless enum
|
|
283
283
|
whose members are only `static let`
|
|
284
284
|
literals (`AppConstant.Phone.defaultDialCode`), where the nesting IS the grouping and
|
|
285
285
|
flattening to `AppConstantPhone` buys nothing. Decide by reference count, never by keyword.
|
|
@@ -291,7 +291,7 @@ rules:
|
|
|
291
291
|
rationale: readability
|
|
292
292
|
check: >
|
|
293
293
|
Scene, ViewModel, LocalizedText, CoordinatorEvent, AnalyticsTracking, UseCase, Repository
|
|
294
|
-
(+protocol +mock), Mapper + models
|
|
294
|
+
(+protocol +mock), Mapper + models - each present when its responsibility exists. Report a
|
|
295
295
|
missing file whose responsibility leaked elsewhere AND a ceremonial empty file.
|
|
296
296
|
|
|
297
297
|
- id: STRUCT-03
|
|
@@ -301,7 +301,7 @@ rules:
|
|
|
301
301
|
rationale: readability
|
|
302
302
|
|
|
303
303
|
- id: STRUCT-04
|
|
304
|
-
title: Nothing lives outside the layout
|
|
304
|
+
title: Nothing lives outside the layout - no loose root type, no Utils/Helpers/Misc bucket
|
|
305
305
|
severity: important
|
|
306
306
|
enforcement: judgement
|
|
307
307
|
rationale: readability
|
|
@@ -315,13 +315,13 @@ rules:
|
|
|
315
315
|
2+ modules -> cross-module shared tier. 2+ screens -> module shared entities. One screen ->
|
|
316
316
|
that screen's own domain folder, in the sub-folder its KIND belongs to: an enum in
|
|
317
317
|
`Domain/Enums/`, a value object in `Domain/Entities/`. A screen-behaviour enum (NAME-05) is
|
|
318
|
-
a domain type, not a presentation one
|
|
318
|
+
a domain type, not a presentation one - it does not live beside the Scene.
|
|
319
319
|
Both over- and under-hoisting are findings: a single-consumer type in the shared tier
|
|
320
320
|
inflates the shared surface and reads as load-bearing when it is not.
|
|
321
321
|
|
|
322
322
|
# ── NAME ──────────────────────────────────────────────────────────────────
|
|
323
323
|
- id: NAME-01
|
|
324
|
-
title: One name per role
|
|
324
|
+
title: One name per role - the navigation handler is never called something else
|
|
325
325
|
severity: blocking
|
|
326
326
|
enforcement: lint
|
|
327
327
|
mechanism: 'custom regex scoped to Scene/ViewModel: \boutput\b as a handler property or parameter'
|
|
@@ -335,7 +335,7 @@ rules:
|
|
|
335
335
|
rationale: flexibility
|
|
336
336
|
|
|
337
337
|
- id: NAME-03
|
|
338
|
-
title: Module naming scheme
|
|
338
|
+
title: Module naming scheme - forbidden affixes come from the module's own conventions doc
|
|
339
339
|
severity: important
|
|
340
340
|
enforcement: lint
|
|
341
341
|
mechanism: custom regex per module (Fetch prefix, Flow suffix, Manager/Helper/Util where banned)
|
|
@@ -357,7 +357,7 @@ rules:
|
|
|
357
357
|
rationale: readability
|
|
358
358
|
check: >
|
|
359
359
|
Where the wire already carries the state (a status, a redirect, a form type), map it into
|
|
360
|
-
an enum and branch on that
|
|
360
|
+
an enum and branch on that - NAME-04 covers the decoding side. Where it does not, but the
|
|
361
361
|
screen still renders materially different variants, declare the enum ON THE SCREEN and
|
|
362
362
|
derive it from whatever inputs decide it. A variant set spread across three or four
|
|
363
363
|
independent booleans is the finding: nothing states which combinations are legal, the
|
|
@@ -368,12 +368,12 @@ rules:
|
|
|
368
368
|
1. Three or more booleans read together to decide one visual outcome.
|
|
369
369
|
2. A boolean pair whose illegal combination is only prevented by call order.
|
|
370
370
|
|
|
371
|
-
The enum belongs to the view model as a derived value, not to stored state
|
|
371
|
+
The enum belongs to the view model as a derived value, not to stored state - deriving it
|
|
372
372
|
keeps a single source of truth, whereas a stored copy drifts from the inputs it mirrors.
|
|
373
373
|
Two independent booleans that never interact are fine; do not enum-ify for its own sake.
|
|
374
374
|
|
|
375
375
|
- id: STRUCT-07
|
|
376
|
-
title: A scene is one type
|
|
376
|
+
title: A scene is one type - no inner view struct wrapping it
|
|
377
377
|
severity: important
|
|
378
378
|
enforcement: judgement
|
|
379
379
|
rationale: readability
|
|
@@ -382,7 +382,7 @@ rules:
|
|
|
382
382
|
body in a second struct in the same file is two types where the reader expects one: the
|
|
383
383
|
scene's name is the one in the coordinator and the file, and everything about the screen
|
|
384
384
|
should be under it. Fold the inner view's body and state into the scene. The exception is a
|
|
385
|
-
genuinely reusable view with its own consumers
|
|
385
|
+
genuinely reusable view with its own consumers - and that one belongs in
|
|
386
386
|
`Presentation/Components/` under READ-04b, not beside the scene.
|
|
387
387
|
|
|
388
388
|
- id: STRUCT-08
|
|
@@ -403,14 +403,14 @@ rules:
|
|
|
403
403
|
check: >
|
|
404
404
|
Form/validation rules are the classic silent duplication: each one is small enough to
|
|
405
405
|
re-type in ten seconds, so every module does, and the copies then drift. Before adding one,
|
|
406
|
-
grep the shared rule library AND every sibling module for the CONCEPT, not the name
|
|
406
|
+
grep the shared rule library AND every sibling module for the CONCEPT, not the name - the
|
|
407
407
|
same rule appears as `EmailFormatRule` / `EmailRule`, `TCKNRule` / `TCKNChecksumRule` /
|
|
408
408
|
`NationalIdRule`, `PnrRule` / `PnrOrETicketFormatRule` / `<Screen>PnrOrETicketFormatRule`.
|
|
409
409
|
Name-matching alone finds none of those.
|
|
410
410
|
|
|
411
411
|
Placement follows STRUCT-05, with one addition: the error copy is NOT part of the rule.
|
|
412
412
|
A rule takes its error as an init parameter, so one shared implementation serves every
|
|
413
|
-
module while each keeps its own localized message
|
|
413
|
+
module while each keeps its own localized message - that is what makes hoisting cheap.
|
|
414
414
|
|
|
415
415
|
Report per concept: implementations · owning modules · whether the shared library already
|
|
416
416
|
has one · and any BEHAVIOURAL divergence between copies. Divergence outranks the
|
|
@@ -418,14 +418,14 @@ rules:
|
|
|
418
418
|
one screen is rejected on another, and nothing in either module says so. Merging them is a
|
|
419
419
|
behaviour decision needing its own tests, never a silent move.
|
|
420
420
|
|
|
421
|
-
# ── SVC
|
|
421
|
+
# ── SVC - service surface ─────────────────────────────────────────────────
|
|
422
422
|
- id: SVC-01
|
|
423
423
|
title: One request model in, one result out
|
|
424
424
|
severity: important
|
|
425
425
|
enforcement: judgement
|
|
426
426
|
rationale: readability
|
|
427
427
|
check: >
|
|
428
|
-
A boundary method past ~2 parameters is a finding
|
|
428
|
+
A boundary method past ~2 parameters is a finding - the parameters want to be a request
|
|
429
429
|
model. async, never a completion handler. No throws alongside the result family. Naming
|
|
430
430
|
is governed by SVC-07, which overrides "pick a domain verb" for anything that wraps a
|
|
431
431
|
single service call.
|
|
@@ -456,7 +456,7 @@ rules:
|
|
|
456
456
|
rationale: testability
|
|
457
457
|
|
|
458
458
|
- id: SVC-06
|
|
459
|
-
title: Spec discipline
|
|
459
|
+
title: Spec discipline - nothing hand-written around the generator, no endpoint in two specs
|
|
460
460
|
severity: important
|
|
461
461
|
enforcement: scan
|
|
462
462
|
rationale: flexibility
|
|
@@ -465,7 +465,7 @@ rules:
|
|
|
465
465
|
title: A method that wraps one service call is named after that service
|
|
466
466
|
severity: important
|
|
467
467
|
enforcement: lint
|
|
468
|
-
mechanism: custom regex
|
|
468
|
+
mechanism: custom regex - non-`send` funcs in repository protocols surface for an explicit marker
|
|
469
469
|
rationale: readability
|
|
470
470
|
check: >
|
|
471
471
|
`send` + the endpoint path, segments in their own order, camel-cased, path parameters
|
|
@@ -474,16 +474,16 @@ rules:
|
|
|
474
474
|
`lookup/{airlineAlliance}/airline` → `sendLookupAirline`.
|
|
475
475
|
The path is the name, not the generated client method. A generator prefixes GET
|
|
476
476
|
endpoints with `get` and appends parameter suffixes (`getSeatMapPageInfo`,
|
|
477
|
-
`getAirlineListByAirlineAlliance`)
|
|
477
|
+
`getAirlineListByAirlineAlliance`) - both are artifacts of the generator, and `get`
|
|
478
478
|
also reorders the segments the backend chose. Copying the generated name silently
|
|
479
479
|
renames the service; copying the path keeps one spelling on both sides of the wire.
|
|
480
480
|
The HTTP verb is never part of the name: the endpoint already implies it, and `send`
|
|
481
481
|
already says a request leaves the device. The name
|
|
482
|
-
carries down the whole chain
|
|
483
|
-
double, and any use case that is a straight pass-through
|
|
482
|
+
carries down the whole chain - repository protocol, live implementation, mock, test
|
|
483
|
+
double, and any use case that is a straight pass-through - so one grep from the
|
|
484
484
|
endpoint reaches every layer that touches it, and a reader of the call site knows
|
|
485
485
|
which service will fire without opening the repository.
|
|
486
|
-
Transport verbs invented by the client (`fetch
|
|
486
|
+
Transport verbs invented by the client (`fetch...`, `load...`, `get...`) are the violation:
|
|
487
487
|
they name what the code does, which the signature already says, instead of what the
|
|
488
488
|
backend is being asked, which nothing else says.
|
|
489
489
|
Two exceptions, and both must be visible:
|
|
@@ -499,24 +499,24 @@ rules:
|
|
|
499
499
|
keeps its domain name (SVC-01).
|
|
500
500
|
|
|
501
501
|
- id: SVC-08
|
|
502
|
-
title: A business rule lives in the view model or on its own struct
|
|
502
|
+
title: A business rule lives in the view model or on its own struct - nowhere else
|
|
503
503
|
severity: important
|
|
504
504
|
enforcement: lint
|
|
505
|
-
mechanism: custom regex
|
|
505
|
+
mechanism: custom regex - arithmetic and conditional expressions inside Mapper paths
|
|
506
506
|
rationale: testability
|
|
507
507
|
check: >
|
|
508
508
|
There are exactly two homes for a business rule: the **view model** that owns the
|
|
509
509
|
behaviour, or a **computed property on the struct the rule is about** (under a
|
|
510
510
|
`// MARK: - Derived`), when more than one screen asks the same question.
|
|
511
511
|
Every other layer is transport. A mapper, a `UseCaseLive`, a repository, a DTO mirror, a
|
|
512
|
-
coordinator, a data source
|
|
512
|
+
coordinator, a data source - each moves values between two shapes and decides nothing. A
|
|
513
513
|
`UseCaseLive` in particular reads as a tempting home because it already knows the domain
|
|
514
514
|
vocabulary: it does not get one. Its body is call → map → return, plus cache read/write;
|
|
515
515
|
the moment it branches on a status code, merges two responses by a rule, or defaults a
|
|
516
516
|
value nobody can derive from the payload, that rule has been hidden from the view model
|
|
517
517
|
that lives with it and from every test that would have covered it.
|
|
518
518
|
A mapper lowers one shape onto another and nothing else. Unwrapping an optional wire
|
|
519
|
-
field to its empty value (`?? ""`, `?? false`, `?? []`) is lowering and is fine
|
|
519
|
+
field to its empty value (`?? ""`, `?? false`, `?? []`) is lowering and is fine - the
|
|
520
520
|
domain type is non-optional and the wire type is not.
|
|
521
521
|
These are NOT lowering, and each one belongs to the view model (or, when several screens
|
|
522
522
|
share it, a named domain rule the view models call):
|
|
@@ -539,7 +539,7 @@ rules:
|
|
|
539
539
|
index refers to, or grouping rows by a key. It moves values into position; it does not
|
|
540
540
|
decide what they mean.
|
|
541
541
|
When the SAME lowering appears in more than one screen's mapper, it does not become a
|
|
542
|
-
shared mapper type in a folder of its own
|
|
542
|
+
shared mapper type in a folder of its own - it moves onto the entity it produces, as an
|
|
543
543
|
`init(_ dto:)` in an extension under a `// MARK: - Wire Mapping` in the entity's own file.
|
|
544
544
|
The entity is where a reader looks for "how is this built"; a `FooMapper` parked at the top
|
|
545
545
|
of the module is a second place to look and belongs to no screen.
|
|
@@ -550,17 +550,17 @@ rules:
|
|
|
550
550
|
So: first make the entity CARRY the raw inputs the decision reads (`info: Info?`,
|
|
551
551
|
`hasPrevention: Bool`, `apisCompletions: [Bool]`), then express the decision as a computed
|
|
552
552
|
property. Where that property lives:
|
|
553
|
-
- **On the entity**, under a `// MARK: - Derived`
|
|
553
|
+
- **On the entity**, under a `// MARK: - Derived` - when more than one screen, or the
|
|
554
554
|
screen AND its mocks, ask the same question. This is also what keeps the mock
|
|
555
555
|
repositories honest: a mock can no longer hand-set a state the real payload could never
|
|
556
556
|
produce, because the state is computed from the same inputs in both.
|
|
557
|
-
- **In the view model**
|
|
557
|
+
- **In the view model** - when the question is that one screen's, or when the answer needs
|
|
558
558
|
anything the entity does not hold (localized copy, a feature flag, live user state).
|
|
559
559
|
Never in the mapper, and never as a stored field the mapper fills, because a stored field
|
|
560
560
|
is indistinguishable from a wire value at every call site that reads it.
|
|
561
561
|
|
|
562
562
|
Auditing a module for this is a grep, but a noisy one. These MATCH the search and are NOT
|
|
563
|
-
violations
|
|
563
|
+
violations - do not "fix" them: `init(x: T? = nil)` default-nil signatures, `?? ""` /
|
|
564
564
|
`?? false` / `?? []` lowering of an optional wire field onto a non-optional domain one,
|
|
565
565
|
`.first { $0.index == requested }` and `Dictionary(grouping:)` shape reads, and
|
|
566
566
|
`.filter { !$0.isEmpty }.joined()` name assembly. What IS a violation: a comparison that
|
|
@@ -572,18 +572,18 @@ rules:
|
|
|
572
572
|
title: Platform affordances go through the app's own wrapper, not the OS API
|
|
573
573
|
severity: blocking
|
|
574
574
|
enforcement: lint
|
|
575
|
-
mechanism: custom regex
|
|
575
|
+
mechanism: custom regex - raw SwiftUI presentation APIs outside the shared wrapper's own module
|
|
576
576
|
rationale: flexibility
|
|
577
577
|
check: >
|
|
578
|
-
When the app ships a wrapper for a platform affordance
|
|
578
|
+
When the app ships a wrapper for a platform affordance - a sheet, a date picker, a toast -
|
|
579
579
|
a feature module calls the wrapper, never the OS API underneath. The wrapper exists
|
|
580
580
|
because the OS changed the affordance once already and will again: iOS 26 forced a
|
|
581
581
|
Liquid-Glass inset onto partial-detent sheets with no opt-out, which put a gap down the
|
|
582
582
|
side and bottom of every raw `.sheet` and swallowed a pushed picker's selection.
|
|
583
583
|
The tell that this rule was skipped: one module's sheets look different from the rest of
|
|
584
584
|
the app after an OS upgrade, and the fix already exists in the shared layer.
|
|
585
|
-
Exception: an affordance the OS itself owns end to end
|
|
586
|
-
sheet, a photo picker
|
|
585
|
+
Exception: an affordance the OS itself owns end to end - a share sheet, an Add-to-Siri
|
|
586
|
+
sheet, a photo picker - stays on the system API. Wrapping those changes nothing and
|
|
587
587
|
breaks their behaviour.
|
|
588
588
|
|
|
589
589
|
- id: PLAT-02
|
|
@@ -593,26 +593,26 @@ rules:
|
|
|
593
593
|
rationale: readability
|
|
594
594
|
check: >
|
|
595
595
|
A content-sized detent asks the content how tall it is. A plain geometry read inside the
|
|
596
|
-
presentation answers with the frame the presentation already gave it
|
|
596
|
+
presentation answers with the frame the presentation already gave it - so the sheet
|
|
597
597
|
latches onto its own current height and never grows. Either publish the height the way
|
|
598
598
|
the framework expects (per region, the way the shared scaffold does) or use a fixed
|
|
599
599
|
height. Do not mix: a content detent with no reporter is a sheet that opens at the wrong
|
|
600
600
|
size in one locale and looks right in another.
|
|
601
601
|
The mirror-image mistake is worse, because it looks like a fix: pre-sizing the content so
|
|
602
602
|
it "reports a better height". A `fixedSize(vertical:)` at the presentation root measures
|
|
603
|
-
the content against an unconstrained WIDTH proposal
|
|
603
|
+
the content against an unconstrained WIDTH proposal - every line of copy lays out on one
|
|
604
604
|
line, and the height that comes back is a fraction of the real one. The sheet then opens
|
|
605
605
|
at two lines tall with the text clipped. When the presentation already measures its
|
|
606
606
|
content (an intrinsic-size host, a per-region scaffold), the content must add NOTHING:
|
|
607
607
|
no `fixedSize`, no geometry read, no frame. Check which key the engine actually observes
|
|
608
|
-
before publishing one
|
|
608
|
+
before publishing one - publishing to a key nothing reads is dead code that reads as a fix.
|
|
609
609
|
A content-sized presentation is also only as good as WHEN it measures. If the panel reads
|
|
610
|
-
the content's intrinsic size during the present animation
|
|
611
|
-
final width
|
|
610
|
+
the content's intrinsic size during the present animation - before the hosting view has its
|
|
611
|
+
final width - copy that wraps reports a fraction of its real height and the sheet opens
|
|
612
612
|
short. It cannot recover on its own when the follow-up measurement reads the RENDERED
|
|
613
613
|
height, because the short panel is already clamping that: the wrong size is a fixed point
|
|
614
614
|
until the user drags the sheet, which is exactly what the bug report will describe. The fix
|
|
615
|
-
belongs in the presentation engine
|
|
615
|
+
belongs in the presentation engine - one re-read after the transition settles - not in each
|
|
616
616
|
caller. The tell that it is a measurement-timing bug and not a detent choice: a wheel picker
|
|
617
617
|
in the same engine is fine, because its ideal height does not depend on width.
|
|
618
618
|
A fixed detent is not the workaround either. `[.medium, .large]` on short content leaves
|
|
@@ -626,13 +626,13 @@ rules:
|
|
|
626
626
|
content must be able to answer it. Two answers that are not answers, and each produces the
|
|
627
627
|
opposite symptom:
|
|
628
628
|
- **A trailing `Spacer`** answers "all of it". The panel opens at its cap with the real
|
|
629
|
-
content stranded at the top. A content-sized sheet's root stack has no `Spacer`
|
|
629
|
+
content stranded at the top. A content-sized sheet's root stack has no `Spacer` - its own
|
|
630
630
|
height IS the answer; use padding for the bottom clearance.
|
|
631
631
|
- **Copy with no width** answers "one line per paragraph". Height that is entirely a
|
|
632
632
|
function of where lines break cannot be measured without a width, so the sheet opens
|
|
633
633
|
short and clips. Hand the presenting screen's measured width down to the content.
|
|
634
634
|
Diagnose by comparing against a sheet in the same engine that works. If one is fine and
|
|
635
|
-
another is not, the engine is not the bug
|
|
635
|
+
another is not, the engine is not the bug - and a shared-engine change to fix one screen
|
|
636
636
|
puts every other domain's sheets at risk for a fix nobody has verified. Fix it where the
|
|
637
637
|
content is.
|
|
638
638
|
When a presentation moves to a different engine, DELETE the previous engine's modifiers
|
|
@@ -654,7 +654,7 @@ rules:
|
|
|
654
654
|
An image or asset URL pointing at a third-party host is a finding even when it renders
|
|
655
655
|
correctly: the host is outside our control and versioning, it can change or disappear
|
|
656
656
|
under us, and every render leaks a request to a party the user never agreed to. The
|
|
657
|
-
carrier logo, the flag, the banner
|
|
657
|
+
carrier logo, the flag, the banner - they all exist on the app's own CDN. Put the URL in
|
|
658
658
|
the module's constants namespace so there is one place to change it.
|
|
659
659
|
|
|
660
660
|
# ── SAFE ──────────────────────────────────────────────────────────────────
|
|
@@ -672,7 +672,7 @@ rules:
|
|
|
672
672
|
mechanism: 'custom regex: DateFormatter constructed in a mapper or repository path'
|
|
673
673
|
rationale: security
|
|
674
674
|
|
|
675
|
-
# ── VIS
|
|
675
|
+
# ── VIS - declaration & visibility ────────────────────────────────────────
|
|
676
676
|
- id: VIS-01
|
|
677
677
|
title: Every class is final unless a real subclass exists in the module
|
|
678
678
|
severity: important
|
|
@@ -684,7 +684,7 @@ rules:
|
|
|
684
684
|
title: Explicit access level, private by default; internal handled consistently
|
|
685
685
|
severity: important
|
|
686
686
|
enforcement: lint
|
|
687
|
-
mechanism: swiftlint private_outlet, explicit_acl (opt-in)
|
|
687
|
+
mechanism: swiftlint private_outlet, explicit_acl (opt-in) - scoped to the module's convention
|
|
688
688
|
rationale: readability
|
|
689
689
|
|
|
690
690
|
- id: VIS-03
|
|
@@ -700,7 +700,7 @@ rules:
|
|
|
700
700
|
enforcement: lint
|
|
701
701
|
mechanism: swiftlint missing_docs scoped to public declarations
|
|
702
702
|
rationale: flexibility
|
|
703
|
-
check: The one place the no-unnecessary-comments rule inverts
|
|
703
|
+
check: The one place the no-unnecessary-comments rule inverts - a cross-module contract cannot explain itself through naming.
|
|
704
704
|
|
|
705
705
|
# ── UI ────────────────────────────────────────────────────────────────────
|
|
706
706
|
- id: UI-01
|
|
@@ -729,21 +729,21 @@ rules:
|
|
|
729
729
|
mechanism: 'custom regex: raw localization key access inside Scene/ViewModel'
|
|
730
730
|
rationale: flexibility
|
|
731
731
|
|
|
732
|
-
# ── MOD
|
|
732
|
+
# ── MOD - modularity ──────────────────────────────────────────────────────
|
|
733
733
|
- id: MOD-01
|
|
734
734
|
title: A feature module never imports another feature module
|
|
735
735
|
severity: blocking
|
|
736
736
|
enforcement: lint
|
|
737
737
|
mechanism: 'custom regex per module: ^import (<sibling feature names>)'
|
|
738
738
|
rationale: flexibility
|
|
739
|
-
check: A feature that imports many siblings to compose them has promoted itself to a second composition root
|
|
739
|
+
check: A feature that imports many siblings to compose them has promoted itself to a second composition root - highest severity in this section.
|
|
740
740
|
|
|
741
741
|
- id: MOD-02
|
|
742
742
|
title: The manifest graph is one-way and acyclic; core never depends on a feature
|
|
743
743
|
severity: blocking
|
|
744
744
|
enforcement: scan
|
|
745
745
|
rationale: flexibility
|
|
746
|
-
check: Check as policy, not by trusting the build
|
|
746
|
+
check: Check as policy, not by trusting the build - a package-level cycle can still build. Report dead edges (declared, never imported).
|
|
747
747
|
|
|
748
748
|
- id: MOD-03
|
|
749
749
|
title: Shared values walk the placement ladder, cheapest rung first, with a stated reason to climb
|
|
@@ -752,7 +752,7 @@ rules:
|
|
|
752
752
|
rationale: flexibility
|
|
753
753
|
|
|
754
754
|
- id: MOD-04
|
|
755
|
-
title: Removability delta
|
|
755
|
+
title: Removability delta - removing the module touches only the composition root
|
|
756
756
|
severity: blocking
|
|
757
757
|
enforcement: scan
|
|
758
758
|
rationale: flexibility
|
|
@@ -768,7 +768,7 @@ rules:
|
|
|
768
768
|
runtime disguise. The composition root is the only place that knows the module list.
|
|
769
769
|
|
|
770
770
|
- id: MOD-06
|
|
771
|
-
title: The public surface is the plug
|
|
771
|
+
title: The public surface is the plug - entry types only, everything else internal
|
|
772
772
|
severity: important
|
|
773
773
|
enforcement: scan
|
|
774
774
|
rationale: flexibility
|
|
@@ -780,7 +780,7 @@ rules:
|
|
|
780
780
|
enforcement: judgement
|
|
781
781
|
rationale: flexibility
|
|
782
782
|
|
|
783
|
-
# ── CONC
|
|
783
|
+
# ── CONC - concurrency ────────────────────────────────────────────────────
|
|
784
784
|
- id: CONC-01
|
|
785
785
|
title: The isolation policy is one declared decision applied everywhere
|
|
786
786
|
severity: important
|
|
@@ -805,7 +805,7 @@ rules:
|
|
|
805
805
|
check: A growing count silently returns the module to pre-Swift-6 guarantees while the build stays green.
|
|
806
806
|
|
|
807
807
|
- id: CONC-04
|
|
808
|
-
title: One concurrency model
|
|
808
|
+
title: One concurrency model - no GCD, semaphore or completion handler layered onto async
|
|
809
809
|
severity: important
|
|
810
810
|
enforcement: lint
|
|
811
811
|
mechanism: 'custom regex: DispatchQueue|DispatchSemaphore|DispatchGroup in async-adopted paths'
|
|
@@ -817,7 +817,7 @@ rules:
|
|
|
817
817
|
enforcement: judgement
|
|
818
818
|
rationale: testability
|
|
819
819
|
|
|
820
|
-
# ── TEST
|
|
820
|
+
# ── TEST - testability by design ──────────────────────────────────────────
|
|
821
821
|
- id: TEST-01
|
|
822
822
|
title: The environment is injected, never reached for
|
|
823
823
|
severity: blocking
|
|
@@ -827,7 +827,7 @@ rules:
|
|
|
827
827
|
check: >
|
|
828
828
|
Time, calendar, randomness, UUID, locale, storage, network, feature flags and session state
|
|
829
829
|
arrive through an abstraction. A logic type calling these directly is untestable by
|
|
830
|
-
construction
|
|
830
|
+
construction - no amount of test-writing discipline fixes it.
|
|
831
831
|
|
|
832
832
|
- id: TEST-02
|
|
833
833
|
title: Business rules are callable without a view, navigation or network
|
|
@@ -844,14 +844,14 @@ rules:
|
|
|
844
844
|
rationale: testability
|
|
845
845
|
|
|
846
846
|
- id: TEST-04
|
|
847
|
-
title: Test doubles follow one named taxonomy
|
|
847
|
+
title: Test doubles follow one named taxonomy - stub, spy, fake, mock
|
|
848
848
|
severity: suggestion
|
|
849
849
|
enforcement: judgement
|
|
850
850
|
rationale: testability
|
|
851
851
|
check: One kind per file, name states the kind, signature parity with the real type (SVC-02).
|
|
852
852
|
|
|
853
853
|
- id: TEST-05
|
|
854
|
-
title: Async behaviour is testable
|
|
854
|
+
title: Async behaviour is testable - no unstructured task in logic, no sleep, injected clock
|
|
855
855
|
severity: important
|
|
856
856
|
enforcement: lint
|
|
857
857
|
mechanism: 'custom regex: Task\s*\{ in logic paths; sleep\( in test paths'
|
|
@@ -870,7 +870,7 @@ rules:
|
|
|
870
870
|
enforcement: scan
|
|
871
871
|
rationale: testability
|
|
872
872
|
|
|
873
|
-
# ── FLEX
|
|
873
|
+
# ── FLEX - intra-module flexibility ───────────────────────────────────────
|
|
874
874
|
- id: FLEX-01
|
|
875
875
|
title: Layers meet through protocols; no concrete cross-layer type in a signature
|
|
876
876
|
severity: important
|
|
@@ -885,7 +885,7 @@ rules:
|
|
|
885
885
|
check: A conditional whose branches differ only in tokens or copy is a finding.
|
|
886
886
|
|
|
887
887
|
- id: FLEX-03
|
|
888
|
-
title: Components are open for extension
|
|
888
|
+
title: Components are open for extension - a new variant does not edit the existing body
|
|
889
889
|
severity: important
|
|
890
890
|
enforcement: judgement
|
|
891
891
|
rationale: flexibility
|
|
@@ -898,12 +898,12 @@ rules:
|
|
|
898
898
|
rationale: flexibility
|
|
899
899
|
|
|
900
900
|
- id: FLEX-05
|
|
901
|
-
title: No layer's vocabulary leaks into another
|
|
901
|
+
title: No layer's vocabulary leaks into another - presentation types never travel downward
|
|
902
902
|
severity: important
|
|
903
903
|
enforcement: judgement
|
|
904
904
|
rationale: flexibility
|
|
905
905
|
|
|
906
|
-
# ── SEC
|
|
906
|
+
# ── SEC - security & privacy ──────────────────────────────────────────────
|
|
907
907
|
- id: SEC-01
|
|
908
908
|
title: Sensitive data persists only when it must, and then only in the Keychain
|
|
909
909
|
severity: blocking
|
|
@@ -916,17 +916,17 @@ rules:
|
|
|
916
916
|
rationale: security
|
|
917
917
|
applies_to_classes: all
|
|
918
918
|
check: >
|
|
919
|
-
Walk persistence_decision first. Most sensitive values in a flow are used and dropped
|
|
919
|
+
Walk persistence_decision first. Most sensitive values in a flow are used and dropped -
|
|
920
920
|
those stay in memory, and writing them to the Keychain is itself a finding, because an
|
|
921
921
|
unnecessary keychain item outlives the flow, survives logout unless someone deletes it, and
|
|
922
922
|
creates a cleanup obligation with no owner. Only a value that must survive app restart is
|
|
923
923
|
persisted, and then: Keychain only, with an explicit accessibility class matching the data
|
|
924
924
|
(device-only unless a documented reason exists), no iCloud sync, biometric or passcode
|
|
925
925
|
gating where policy requires. never-persist-locally classes stay transient regardless.
|
|
926
|
-
Report both over-persistence and under-protection
|
|
926
|
+
Report both over-persistence and under-protection - they are equally findings.
|
|
927
927
|
|
|
928
928
|
- id: SEC-02
|
|
929
|
-
title: No hardcoded secrets
|
|
929
|
+
title: No hardcoded secrets - anything in source is treated as already leaked
|
|
930
930
|
severity: blocking
|
|
931
931
|
enforcement: lint
|
|
932
932
|
mechanism: 'custom regex: (api[_-]?key|secret|password|bearer|private[_-]?key)\s*[:=]\s*"'
|
|
@@ -945,7 +945,7 @@ rules:
|
|
|
945
945
|
check: >
|
|
946
946
|
Every class marked loggable:never is redacted; membership-identity is hashed or truncated
|
|
947
947
|
where it must appear at all. Interpolated values are private by default and only values
|
|
948
|
-
explicitly known to be non-sensitive are made public
|
|
948
|
+
explicitly known to be non-sensitive are made public - the default must fail safe, because
|
|
949
949
|
the cost of a missed annotation is a logged credential.
|
|
950
950
|
|
|
951
951
|
- id: SEC-04
|
|
@@ -976,7 +976,7 @@ rules:
|
|
|
976
976
|
severity: important
|
|
977
977
|
enforcement: scan
|
|
978
978
|
rationale: security
|
|
979
|
-
check: A permission requested but unused is a finding
|
|
979
|
+
check: A permission requested but unused is a finding - a privacy problem and an App Review risk.
|
|
980
980
|
|
|
981
981
|
- id: SEC-08
|
|
982
982
|
title: The privacy manifest is complete and honest
|
|
@@ -990,9 +990,9 @@ rules:
|
|
|
990
990
|
severity: blocking
|
|
991
991
|
enforcement: lint
|
|
992
992
|
mechanism: >
|
|
993
|
-
custom regex for the commented-out compilation directive only (a "// #if" is not a guard
|
|
993
|
+
custom regex for the commented-out compilation directive only (a "// #if" is not a guard -
|
|
994
994
|
the code inside it ships). Whether an affordance is actually gated is JUDGEMENT: once the
|
|
995
|
-
gate is a resolved capability rather than a compile-time condition, no regex can see it
|
|
995
|
+
gate is a resolved capability rather than a compile-time condition, no regex can see it -
|
|
996
996
|
a pattern that looked for "#if DEBUG nearby" flags the gated call site and the debug type's
|
|
997
997
|
own declaration alike, which is noise, not signal.
|
|
998
998
|
rationale: security
|
|
@@ -1003,30 +1003,30 @@ rules:
|
|
|
1003
1003
|
mechanism that actually fires there is acceptable.
|
|
1004
1004
|
|
|
1005
1005
|
Three failure shapes, all findings:
|
|
1006
|
-
1. No gate
|
|
1007
|
-
2. A commented-out gate
|
|
1006
|
+
1. No gate - the affordance is unconditionally compiled and reachable.
|
|
1007
|
+
2. A commented-out gate - "// #if DEBUG" reads like protection and provides none. Usually a
|
|
1008
1008
|
symptom of shape 3 rather than carelessness.
|
|
1009
|
-
3. A gate that fires in the wrong builds
|
|
1009
|
+
3. A gate that fires in the wrong builds - "#if DEBUG" in a Release-configured internal
|
|
1010
1010
|
build strips the affordance from exactly the testers who need it, so someone eventually
|
|
1011
1011
|
comments it out and it reaches the store. Diagnose this as a missing mechanism, never as
|
|
1012
1012
|
"restore the directive": restoring it re-breaks the internal build.
|
|
1013
1013
|
|
|
1014
1014
|
Know the constraint before proposing a fix. A SwiftPM module does not inherit the app
|
|
1015
1015
|
target's SWIFT_ACTIVE_COMPILATION_CONDITIONS, and SwiftPM's .when(configuration:) predicate
|
|
1016
|
-
distinguishes only debug from release
|
|
1016
|
+
distinguishes only debug from release - a custom Xcode configuration is invisible inside the
|
|
1017
1017
|
package. Where a module needs a three-way distinction (local / internal / store), a
|
|
1018
1018
|
compile-time condition alone cannot express it.
|
|
1019
1019
|
|
|
1020
1020
|
Prefer, in order: (a) the composition root resolves the distribution channel once and
|
|
1021
1021
|
injects it as a capability the module consumes through an abstraction, so the module never
|
|
1022
|
-
knows how it was distributed
|
|
1022
|
+
knows how it was distributed - this also satisfies MOD-05; (b) a build-injected Info.plist
|
|
1023
1023
|
flag read at startup; (c) a runtime channel probe such as the absence of an embedded
|
|
1024
1024
|
provisioning profile. Whichever is chosen it must be ONE shared mechanism: one per module
|
|
1025
1025
|
means nobody can answer "is this in the store build?" without reading every call site.
|
|
1026
1026
|
|
|
1027
|
-
# ── DEPR
|
|
1027
|
+
# ── DEPR - deprecation debt ───────────────────────────────────────────────
|
|
1028
1028
|
- id: DEPR-01
|
|
1029
|
-
title: No call site of a deprecated API
|
|
1029
|
+
title: No call site of a deprecated API - platform or in-repo
|
|
1030
1030
|
severity: important
|
|
1031
1031
|
enforcement: scan
|
|
1032
1032
|
rationale: flexibility
|
|
@@ -1036,7 +1036,7 @@ rules:
|
|
|
1036
1036
|
from the build the module is actually verified with (WARN-01) rather than guessing. In-repo
|
|
1037
1037
|
deprecations are greppable: find every `@available(*, deprecated)` declaration across the
|
|
1038
1038
|
repo, then count this module's call sites of each.
|
|
1039
|
-
A deprecation nobody migrates is worse than no deprecation
|
|
1039
|
+
A deprecation nobody migrates is worse than no deprecation - it trains readers to ignore the
|
|
1040
1040
|
warning, and it hides the one that matters. Report per deprecated symbol: declaration site ·
|
|
1041
1041
|
replacement named in the message · this module's call-site count · migration owner.
|
|
1042
1042
|
|
|
@@ -1073,7 +1073,7 @@ rules:
|
|
|
1073
1073
|
An `if #available(iOS 16)` in a module that already requires iOS 17 is dead branching a
|
|
1074
1074
|
reader must still evaluate. Forward gates (above the target) are legitimate and stay.
|
|
1075
1075
|
|
|
1076
|
-
# ── WARN
|
|
1076
|
+
# ── WARN - warning debt ───────────────────────────────────────────────────
|
|
1077
1077
|
- id: WARN-01
|
|
1078
1078
|
title: The module's build produces zero warnings, and the count never rises
|
|
1079
1079
|
severity: important
|
|
@@ -1081,12 +1081,12 @@ rules:
|
|
|
1081
1081
|
rationale: readability
|
|
1082
1082
|
metric: "compiler warnings for this module (target 0, hard requirement: not increasing)"
|
|
1083
1083
|
check: >
|
|
1084
|
-
Harvest from the module's real verification build (an xcresult or a build log
|
|
1084
|
+
Harvest from the module's real verification build (an xcresult or a build log - the one the
|
|
1085
1085
|
team actually runs, since some targets cannot be built from the CLI). Group by warning kind
|
|
1086
1086
|
and report the top kinds, not just a total: one repeated warning across 40 files is a single
|
|
1087
1087
|
fix, and a total hides that.
|
|
1088
1088
|
Warnings are the canary for deprecation, concurrency and unused-code debt at once. A build
|
|
1089
|
-
with 300 warnings has no working warning channel
|
|
1089
|
+
with 300 warnings has no working warning channel - the next real one is invisible, which is
|
|
1090
1090
|
the actual cost. Treat the count as a ratchet even when zero is out of reach today.
|
|
1091
1091
|
|
|
1092
1092
|
- id: WARN-02
|
|
@@ -1097,7 +1097,7 @@ rules:
|
|
|
1097
1097
|
rationale: readability
|
|
1098
1098
|
metric: "untagged TODO count (target 0), FIXME count (target 0)"
|
|
1099
1099
|
check: >
|
|
1100
|
-
`// TODO[SWAGGER-208458421]:
|
|
1100
|
+
`// TODO[SWAGGER-208458421]: ...` is a tracked decision; a bare `// TODO:` is a note to a
|
|
1101
1101
|
person who has left. Tagging is cheap and makes the debt countable. FIXME means "known
|
|
1102
1102
|
broken" and belongs in the tracker, not the source.
|
|
1103
1103
|
|
|
@@ -1135,7 +1135,7 @@ rules:
|
|
|
1135
1135
|
mechanism: 'custom regex: \.left|\.right in alignment and padding edges'
|
|
1136
1136
|
rationale: accessibility
|
|
1137
1137
|
|
|
1138
|
-
# ── PERF
|
|
1138
|
+
# ── PERF - minimal set; the boundary is declared in STANDARD.md ───────────
|
|
1139
1139
|
- id: PERF-01
|
|
1140
1140
|
title: No expensive computation inside a view body
|
|
1141
1141
|
severity: important
|
|
@@ -1150,7 +1150,7 @@ rules:
|
|
|
1150
1150
|
rationale: performance
|
|
1151
1151
|
|
|
1152
1152
|
- id: PERF-03
|
|
1153
|
-
title: No blocking work at init or on the main actor
|
|
1153
|
+
title: No blocking work at init or on the main actor - decoding, file I/O, JSON
|
|
1154
1154
|
severity: important
|
|
1155
1155
|
enforcement: judgement
|
|
1156
1156
|
rationale: performance
|