@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.
@@ -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 renumbereda rule is retired by status, not deletion.
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: "Noassume transient until a requirement says otherwise"
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 surviveapp backgrounding, app restart, or reinstall?
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 saidthe answer to "it needs to survive"
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 ladderit says
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
- # ── READformat & readability ───────────────────────────────────────────
103
+ # ── READ - format & readability ───────────────────────────────────────────
104
104
  - id: READ-01
105
- title: MARK sections separate concernsbusiness rules, service calls, UI, one group each
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
- `Service requests`, `On-device cache`, and so onso a reader sees at a glance which
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 asidenever the decision. If a sentence records WHY a shape was chosen, or what
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-outa comment that IS data is not prose and is not trimmed: a trigger/prefix table,
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-outsscreen 600 / file 400 / function 40 lines
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 configthose files still report. Honour the exemption in the
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 findinga reader has no map.
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 headera route chip, a countdown, a step bar
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
- THINGa switcher, a chip row, a banner, a bar, a legend, a cardextract it to its own
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 itselfthe piece that orders
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"is the header shown", "which leg is active", "what does the CTA say"is a
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 viewa date
212
- format, a duration split, initials from a name, a unit conversionis a value transform.
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 namethe existing helper will be spelled
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 primitivesnaming which two formats a screen's pickers exchange, for instance
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 styleopen paren at line end, one parameter per line
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 constructsforce unwrap, force cast, IUO, magic numbers, raw colors/fonts, print
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 headerown target module, and the author's FULL git identity
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
- # ── STRUCTproject structure ────────────────────────────────────────────
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 ownsa `@FormSection`-generated
280
- `X.Section`, a Code Connect-mapped `X.State`where flattening silently renames a
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 namespacea caseless enum
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 + modelseach present when its responsibility exists. Report a
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 layoutno loose root type, no Utils/Helpers/Misc bucket
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 oneit does not live beside the Scene.
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 rolethe navigation handler is never called something else
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 schemeforbidden affixes come from the module's own conventions doc
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 thatNAME-04 covers the decoding side. Where it does not, but the
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 statederiving it
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 typeno inner view struct wrapping it
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 consumersand that one belongs in
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 namethe
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 messagethat is what makes hoisting cheap.
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
- # ── SVCservice surface ─────────────────────────────────────────────────
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 findingthe parameters want to be a request
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 disciplinenothing hand-written around the generator, no endpoint in two specs
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 regexnon-`send` funcs in repository protocols surface for an explicit marker
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`)both are artifacts of the generator, and `get`
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 chainrepository protocol, live implementation, mock, test
483
- double, and any use case that is a straight pass-throughso one grep from the
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…`, `load…`, `get…`) are the violation:
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 structnowhere else
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 regexarithmetic and conditional expressions inside Mapper paths
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 sourceeach moves values between two shapes and decides nothing. A
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 finethe
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 ownit moves onto the entity it produces, as an
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`when more than one screen, or the
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**when the question is that one screen's, or when the answer needs
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
- violationsdo not "fix" them: `init(x: T? = nil)` default-nil signatures, `?? ""` /
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 regexraw SwiftUI presentation APIs outside the shared wrapper's own module
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 affordancea sheet, a date picker, a toast
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 enda share sheet, an Add-to-Siri
586
- sheet, a photo pickerstays on the system API. Wrapping those changes nothing and
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 itso the sheet
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 proposalevery line of copy lays out on one
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 onepublishing to a key nothing reads is dead code that reads as a fix.
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 animationbefore the hosting view has its
611
- final widthcopy that wraps reports a fraction of its real height and the sheet opens
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 engineone re-read after the transition settlesnot in each
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`its own
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 bugand a shared-engine change to fix one screen
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 bannerthey all exist on the app's own CDN. Put the URL in
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
- # ── VISdeclaration & visibility ────────────────────────────────────────
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)scoped to the module's convention
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 invertsa cross-module contract cannot explain itself through naming.
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
- # ── MODmodularity ──────────────────────────────────────────────────────
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 roothighest severity in this section.
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 builda package-level cycle can still build. Report dead edges (declared, never imported).
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 deltaremoving the module touches only the composition root
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 plugentry types only, everything else internal
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
- # ── CONCconcurrency ────────────────────────────────────────────────────
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 modelno GCD, semaphore or completion handler layered onto async
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
- # ── TESTtestability by design ──────────────────────────────────────────
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
- constructionno amount of test-writing discipline fixes it.
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 taxonomystub, spy, fake, mock
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 testableno unstructured task in logic, no sleep, injected clock
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
- # ── FLEXintra-module flexibility ───────────────────────────────────────
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 extensiona new variant does not edit the existing body
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 anotherpresentation types never travel downward
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
- # ── SECsecurity & privacy ──────────────────────────────────────────────
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-protectionthey are equally findings.
926
+ Report both over-persistence and under-protection - they are equally findings.
927
927
 
928
928
  - id: SEC-02
929
- title: No hardcoded secretsanything in source is treated as already leaked
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 publicthe default must fail safe, because
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 findinga privacy problem and an App Review risk.
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 gatethe affordance is unconditionally compiled and reachable.
1007
- 2. A commented-out gate"// #if DEBUG" reads like protection and provides none. Usually a
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"#if DEBUG" in a Release-configured internal
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 releasea custom Xcode configuration is invisible inside the
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 distributedthis also satisfies MOD-05; (b) a build-injected Info.plist
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
- # ── DEPRdeprecation debt ───────────────────────────────────────────────
1027
+ # ── DEPR - deprecation debt ───────────────────────────────────────────────
1028
1028
  - id: DEPR-01
1029
- title: No call site of a deprecated APIplatform or in-repo
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 deprecationit trains readers to ignore the
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
- # ── WARNwarning debt ───────────────────────────────────────────────────
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 logthe one the
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 channelthe next real one is invisible, which is
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]: …` is a tracked decision; a bare `// TODO:` is a note to a
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
- # ── PERFminimal set; the boundary is declared in STANDARD.md ───────────
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 actordecoding, file I/O, JSON
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