@mmerterden/multi-agent-pipeline 14.2.2 → 15.1.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.
Files changed (132) hide show
  1. package/CHANGELOG.md +186 -6
  2. package/README.md +19 -12
  3. package/README.tr.md +19 -12
  4. package/SECURITY.md +43 -0
  5. package/docs/FIGMA_PIPELINE.md +3 -3
  6. package/docs/adr/0006-skills-core-external-split.md +1 -1
  7. package/docs/adr/0007-multi-tool-adapter-framework.md +1 -1
  8. package/docs/adr/0009-claude-stack-skills-plugin-only.md +31 -0
  9. package/docs/adr/README.md +1 -0
  10. package/docs/architecture.md +13 -13
  11. package/docs/ecosystem.md +31 -31
  12. package/docs/features.md +5 -5
  13. package/index.js +6 -1
  14. package/install/_codex-agents.mjs +11 -2
  15. package/install/_common.mjs +109 -3
  16. package/install/_dev-only-files.mjs +0 -1
  17. package/install/_platform-filter.mjs +54 -113
  18. package/install/_plugin-skills.mjs +36 -36
  19. package/install/claude.mjs +251 -61
  20. package/install/codex.mjs +28 -6
  21. package/install/copilot.mjs +69 -9
  22. package/install/index.mjs +9 -3
  23. package/install/templates/codex-instructions.md +1 -1
  24. package/install/templates/copilot-instructions.md +3 -3
  25. package/package.json +2 -3
  26. package/pipeline/commands/multi-agent/SKILL.md +2 -0
  27. package/pipeline/commands/multi-agent/analysis/SKILL.md +3 -3
  28. package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
  29. package/pipeline/commands/multi-agent/build-optimize/SKILL.md +9 -9
  30. package/pipeline/commands/multi-agent/channels/SKILL.md +1 -1
  31. package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +186 -0
  32. package/pipeline/commands/multi-agent/dev/SKILL.md +1 -1
  33. package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +1 -1
  34. package/pipeline/commands/multi-agent/dev-local/SKILL.md +1 -1
  35. package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +1 -1
  36. package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
  37. package/pipeline/commands/multi-agent/help/SKILL.md +19 -4
  38. package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +2 -2
  39. package/pipeline/commands/multi-agent/jira/SKILL.md +1 -1
  40. package/pipeline/commands/multi-agent/prune-prompts/SKILL.md +81 -0
  41. package/pipeline/commands/multi-agent/refactor/SKILL.md +36 -1
  42. package/pipeline/commands/multi-agent/resume/SKILL.md +1 -1
  43. package/pipeline/commands/multi-agent/{ship → resume-local}/SKILL.md +8 -8
  44. package/pipeline/commands/multi-agent/scan/SKILL.md +1 -1
  45. package/pipeline/commands/multi-agent/setup/SKILL.md +5 -5
  46. package/pipeline/commands/multi-agent/stack/SKILL.md +62 -40
  47. package/pipeline/commands/multi-agent/store-ready/SKILL.md +3 -3
  48. package/pipeline/commands/multi-agent/sync/SKILL.md +18 -11
  49. package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +1 -1
  50. package/pipeline/commands/multi-agent/uninstall/SKILL.md +2 -0
  51. package/pipeline/commands/multi-agent/update/SKILL.md +4 -4
  52. package/pipeline/lib/issue-fetcher.sh +1 -1
  53. package/pipeline/lib/parse-complaints.sh +316 -0
  54. package/pipeline/multi-agent-refs/channels/wiki.md +3 -3
  55. package/pipeline/multi-agent-refs/complaint-analysis-template.md +99 -0
  56. package/pipeline/multi-agent-refs/component-dispatch.md +6 -6
  57. package/pipeline/multi-agent-refs/cross-cli-contract.md +16 -16
  58. package/pipeline/multi-agent-refs/features/external-context-injection.md +1 -1
  59. package/pipeline/multi-agent-refs/features/stack-skill-routing.md +5 -5
  60. package/pipeline/multi-agent-refs/generate-issue.md +1 -1
  61. package/pipeline/multi-agent-refs/phases/modes.md +1 -1
  62. package/pipeline/multi-agent-refs/phases/operations.md +7 -1
  63. package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
  64. package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +7 -7
  65. package/pipeline/multi-agent-refs/phases/phase-2-planning.md +5 -5
  66. package/pipeline/multi-agent-refs/phases/phase-3-dev.md +3 -3
  67. package/pipeline/multi-agent-refs/phases/phase-4-review.md +12 -12
  68. package/pipeline/multi-agent-refs/phases/phase-5-test.md +1 -1
  69. package/pipeline/multi-agent-refs/phases/phase-7-report.md +6 -0
  70. package/pipeline/multi-agent-refs/tracker-contract.md +3 -2
  71. package/pipeline/multi-agent-refs/wiki-capture.md +2 -2
  72. package/pipeline/preferences-template.json +18 -5
  73. package/pipeline/rules/figma-pipeline.md +2 -2
  74. package/pipeline/schemas/agent-state.schema.json +1 -1
  75. package/pipeline/schemas/complaint-analysis-spec.schema.json +216 -0
  76. package/pipeline/schemas/migrations/prefs-2.5.0-to-2.6.0.mjs +46 -0
  77. package/pipeline/schemas/prefs.schema.json +296 -66
  78. package/pipeline/schemas/token-budget.json +2 -2
  79. package/pipeline/scripts/README.md +4 -3
  80. package/pipeline/scripts/_stack-routing.mjs +79 -0
  81. package/pipeline/scripts/audit-log-rotate.sh +4 -1
  82. package/pipeline/scripts/build-skills-index.mjs +11 -0
  83. package/pipeline/scripts/build-stack-plugins.mjs +28 -60
  84. package/pipeline/scripts/check-derived-drift.mjs +55 -28
  85. package/pipeline/scripts/gc-worktrees.sh +4 -1
  86. package/pipeline/scripts/gen-skills-index.mjs +1 -1
  87. package/pipeline/scripts/match-skills.mjs +12 -2
  88. package/pipeline/scripts/migrate-prefs.mjs +33 -21
  89. package/pipeline/scripts/phase-tracker.sh +32 -5
  90. package/pipeline/scripts/phase0-exit-gate.mjs +3 -2
  91. package/pipeline/scripts/run-aggregator.mjs +7 -2
  92. package/pipeline/scripts/scan-agent-config.sh +1 -1
  93. package/pipeline/scripts/skill-conformance.mjs +165 -30
  94. package/pipeline/scripts/smoke-cross-cli-behavior.sh +1 -1
  95. package/pipeline/scripts/test-gap-rules/android.json +25 -0
  96. package/pipeline/scripts/test-gap-rules/ios.json +34 -0
  97. package/pipeline/scripts/test-gap-rules/node.json +29 -0
  98. package/pipeline/scripts/test-gap-rules/python.json +25 -0
  99. package/pipeline/scripts/uninstall.mjs +160 -11
  100. package/pipeline/scripts/usage-report.mjs +426 -0
  101. package/pipeline/scripts/validate-complaint-doc.mjs +250 -0
  102. package/pipeline/scripts/validate-reviewer.mjs +9 -3
  103. package/pipeline/skills/.skill-manifest.json +156 -108
  104. package/pipeline/skills/.skills-index.json +449 -12
  105. package/pipeline/skills/shared/README.md +14 -10
  106. package/pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md +1 -1
  107. package/pipeline/skills/shared/core/multi-agent-build-optimize/SKILL.md +1 -1
  108. package/pipeline/skills/shared/core/multi-agent-complaint-analysis/SKILL.md +49 -0
  109. package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +1 -1
  110. package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +1 -1
  111. package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +1 -1
  112. package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +1 -1
  113. package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +2 -2
  114. package/pipeline/skills/shared/core/multi-agent-prune-prompts/SKILL.md +83 -0
  115. package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +153 -90
  116. package/pipeline/skills/shared/core/{multi-agent-ship → multi-agent-resume-local}/SKILL.md +6 -6
  117. package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +89 -22
  118. package/pipeline/skills/shared/core/multi-agent-store-ready/SKILL.md +1 -1
  119. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +8 -8
  120. package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +1 -1
  121. package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +1 -1
  122. package/pipeline/skills/shared/external/ios-coding-standard/modules/_TEMPLATE.yml +2 -2
  123. package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +368 -33
  124. package/pipeline/skills/shared/external/ios-coding-standard/references/swiftlint.draft.yml +1 -2
  125. package/pipeline/skills/shared/external/ios-coding-standard/scripts/check_structure.py +765 -0
  126. package/pipeline/skills/shared/external/ios-module-structure/SKILL.md +75 -0
  127. package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +131 -0
  128. package/pipeline/skills/shared/external/ios-module-structure/references/rules.yml +559 -0
  129. package/pipeline/skills/shared/external/ios-module-structure/scripts/check_structure.py +765 -0
  130. package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +53 -10
  131. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +4 -3
  132. package/pipeline/skills/skills-index.md +7 -4
@@ -0,0 +1,559 @@
1
+ version: 0.1.0
2
+ updated: 2026-08-13
3
+ owner: iOS platform
4
+ description: >
5
+ The ios-module-structure rule registry. Where a declaration lives, what its file is called, and what
6
+ its folder must contain beside it. Sibling to the coding-standard registry, which governs file
7
+ CONTENT; this one governs the TREE. IDs are stable and never renumbered - a rule is retired by
8
+ status, not deletion.
9
+
10
+ MATURITY: 0.x. The rule set was generalised from one module's conventions and stress-tested
11
+ against one more; that second contact added two dialect slots. Expect a third module to add
12
+ more. Slots and carve-outs absorb that without breaking a binding, but do not treat this as a
13
+ frozen standard yet.
14
+
15
+ Every rule here is written over ROLES, never over literal paths or type names. A role is bound by
16
+ the module's own overlay. That is what lets one registry serve a module that spells its subview
17
+ folder one way and a module that spells it another, without either becoming a hundred findings.
18
+
19
+ # What these rules may be applied to. A consumer resolves this BEFORE selecting rules and records
20
+ # what it dropped. Per-rule `scope:` narrows this and never widens it.
21
+ scope:
22
+ languages: [swift]
23
+ # The engine reads this: it holds no language of its own.
24
+ sourceExtension: .swift
25
+ paths:
26
+ - "**/*.swift"
27
+ excludePaths:
28
+ - "**/*.generated.swift"
29
+ - "**/Generated/**"
30
+ - "**/*.pb.swift"
31
+ - "**/.build/**"
32
+ - "**/DerivedData/**"
33
+ notCovered:
34
+ objective-c: "*.m / *.mm / *.h - no rules here speak for them; report as a coverage gap"
35
+ resources: "asset catalogs, plists, storyboards - the tree rules do not describe them"
36
+
37
+ severity_levels: [blocking, important, suggestion]
38
+ enforcement_kinds:
39
+ lint: a tool decides it mechanically from the tree and file text
40
+ scan: a tool measures it across the module (counts, ratios, mirrors)
41
+ judgement: requires a human; the tool can only surface the candidate
42
+ exception_marker: "// standard:exception(<RULE-ID>) <reason> <expiry:YYYY-MM-DD>"
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Roles
46
+ # ---------------------------------------------------------------------------
47
+ # A rule names a role; the overlay binds the role to this module's reality. An UNBOUND role
48
+ # disables every rule that reads it, and the audit reports that rather than guessing a shape.
49
+ #
50
+ # Binding forms:
51
+ # glob - a path pattern relative to the module root, e.g. "Sources/*/Screens/*/Presentation/*Scene.swift"
52
+ # derived - a pattern computed from another role's match, e.g. "{dir}/{stem}Configuration.swift"
53
+ # `{dir}`, `{stem}`, `{screen}` and `{target}` are the only substitutions.
54
+ roles:
55
+ screen.root:
56
+ description: the directory that is one screen. Everything else is resolved relative to it.
57
+ required: true
58
+ screen.entry:
59
+ description: the file a coordinator or factory constructs to show the screen.
60
+ screen.viewmodel:
61
+ description: the type holding the screen's behaviour and service calls.
62
+ screen.state:
63
+ description: the type holding the screen's UI state, when the module separates it from the view model.
64
+ screen.analytics:
65
+ description: the screen's analytics surface.
66
+ screen.factory:
67
+ description: the seam another module calls to build the screen.
68
+ screen.mapper:
69
+ description: the wire-to-domain translation for the screen.
70
+ service.dir:
71
+ description: one directory per service operation, holding that operation's models.
72
+ service.request:
73
+ description: the request half of a service operation.
74
+ service.response:
75
+ description: the response half of a service operation.
76
+ subview.dir:
77
+ description: where a screen's extracted views live, when the module extracts them into folders.
78
+ subview.view:
79
+ description: an extracted view belonging to one screen.
80
+ subview.configuration:
81
+ description: the value type an extracted view renders.
82
+ repository.live:
83
+ description: the production implementation of a screen's data access.
84
+ repository.mock:
85
+ description: the offline/scripted implementation used by previews and the debug menu.
86
+ shared.root:
87
+ description: the module's cross-screen folder.
88
+ source.root:
89
+ description: the module's source tree, as the mirror rule's counterpart to the test tree.
90
+ test.root:
91
+ description: the module's test tree.
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # Dialect slots
95
+ # ---------------------------------------------------------------------------
96
+ module_overlay_slots:
97
+ description: >
98
+ Some rules govern a CHOICE rather than a defect: two shapes are each internally coherent, the
99
+ cost is only in mixing them, and picking one is the module's call. Writing one of them into the
100
+ shared registry turns every module that chose the other into a hundred findings - which is a
101
+ migration proposal wearing a structure pass. Those rules bind to a slot here instead. An
102
+ UNBOUND slot disables its rules and the audit says so, rather than defaulting to one dialect
103
+ silently.
104
+
105
+ A slot is only legitimate when both values are genuinely defensible. A module with no analytics
106
+ surface has not chosen a different dialect, it is missing the surface - so STRUCT-04 is a rule,
107
+ not a slot.
108
+ slots:
109
+ - id: ScreenLayerShape
110
+ governs: [STRUCT-01, STRUCT-02]
111
+ values:
112
+ layered: >
113
+ every screen carries the same fixed set of layer folders, including the ones that are
114
+ empty for that screen. The tree teaches the architecture; a missing folder is a finding.
115
+ organic: >
116
+ a screen carries only the layers it actually has. Nothing is created to satisfy a shape,
117
+ and a missing layer means the screen genuinely has no work at that level.
118
+ note: >
119
+ Module-wide. The audit reads it to decide whether "screen has no data layer" is a finding
120
+ or a fact.
121
+ - id: ServiceModelDir
122
+ governs: [STRUCT-03]
123
+ values:
124
+ under-data: the request/response pair sits in the data layer, beside the code that calls the service.
125
+ under-mapper: the pair sits beside the mapper that translates it.
126
+ note: The pairing rule is the same either way; only the parent differs.
127
+ - id: ScreenAssemblyShape
128
+ governs: [STRUCT-05]
129
+ values:
130
+ per-screen-factory: each screen ships its own construction seam.
131
+ shared-factory: one factory per module builds every screen.
132
+ note: >
133
+ Both put construction behind a seam, which is the property STRUCT-05 protects. Counting
134
+ factories against screens tells you which one the module chose.
135
+ - id: UIStateHolder
136
+ governs: [STRUCT-06]
137
+ values:
138
+ separate-state-type: UI state lives in its own type beside the view model.
139
+ view-model-owned: the view model holds UI state directly.
140
+ note: >
141
+ Unbound, STRUCT-06 is disabled - "the view model holds a form field" is only a finding in a
142
+ module that decided it should not.
143
+ - id: SubviewShape
144
+ governs: [STRUCT-07, STRUCT-08, STRUCT-09]
145
+ values:
146
+ folder-per-subview: each extracted view gets a folder holding it and its value type.
147
+ flat: extracted views sit loose in one folder, with no required companion.
148
+ note: >
149
+ The sibling rules only make sense under folder-per-subview. Under flat they are disabled,
150
+ not violated.
151
+ - id: ScreenCompositionShape
152
+ governs: [STRUCT-10]
153
+ values:
154
+ extracted: >
155
+ a fragment of the screen becomes a named view beside it. The entry file is a table of
156
+ contents; each piece can be previewed and snapshotted alone.
157
+ in-file: >
158
+ the entry file composes its own fragments. One file tells the whole screen's story and a
159
+ reader never chases a name across the folder.
160
+ note: >
161
+ Both keep the body readable, which is what STRUCT-10 protects. Counting view members on
162
+ entry files against extracted view files tells you which one the module chose.
163
+ - id: ServiceModelPairing
164
+ governs: [STRUCT-03]
165
+ values:
166
+ both: every operation models a request and a response, even when the request is empty.
167
+ response-only: >
168
+ a request is modelled only when the call carries a body; a bodyless GET has a response
169
+ and nothing else.
170
+ note: >
171
+ Under response-only a lone response is normal and only a lone REQUEST is the finding.
172
+
173
+ - id: CopyResolution
174
+ governs: [VOCAB-06]
175
+ values:
176
+ render-site: copy keys are resolved where the text is rendered.
177
+ copy-layer: a per-screen type owns every string the screen shows.
178
+ note: Both keep copy findable; mixing them is what costs.
179
+
180
+ # ---------------------------------------------------------------------------
181
+ # Rules
182
+ # ---------------------------------------------------------------------------
183
+ rules:
184
+ # --- Tree ---------------------------------------------------------------
185
+ - id: STRUCT-01
186
+ title: A screen carries the layer folders its module's shape declares
187
+ severity: important
188
+ enforcement: lint
189
+ predicate: dir_required_in_dir
190
+ params:
191
+ slot: ScreenLayerShape
192
+ slot_value: layered
193
+ vocabulary_key: LayerDirs
194
+ applies_when: the module binds ScreenLayerShape. Unbound, this rule is DISABLED.
195
+ applies_to_value: layered
196
+ rationale: navigability
197
+ check: >
198
+ Under `layered`, a screen missing one of the declared layer folders is a finding: the tree is
199
+ the architecture diagram and a hole in it makes the reader guess. Under `organic` this rule
200
+ does not run at all.
201
+
202
+ - id: STRUCT-02
203
+ title: A screen's presentation layer is never absent
204
+ severity: blocking
205
+ enforcement: lint
206
+ predicate: dir_required_in_dir
207
+ params:
208
+ vocabulary_key: PresentationDir
209
+ rationale: navigability
210
+ check: >
211
+ Whatever else a screen does or does not carry, it renders something. A screen directory with
212
+ no presentation layer is either dead or misfiled; both are findings.
213
+
214
+ - id: STRUCT-03
215
+ title: A service operation's request and response live together, and neither travels alone
216
+ severity: important
217
+ enforcement: lint
218
+ predicate: pair_required_in_dir
219
+ params:
220
+ slot: ServiceModelDir
221
+ container_role: service.dir
222
+ left_role: service.request
223
+ right_role: service.response
224
+ pairing_slot: ServiceModelPairing
225
+ right_only_value: response-only
226
+ applies_when: the module binds ServiceModelDir. Unbound, this rule is DISABLED.
227
+ rationale: navigability
228
+ check: >
229
+ A directory that holds one half of a service operation must hold the other. A lone response
230
+ means the request is inlined somewhere a reader will not find it, and a lone request means
231
+ the response is being decoded into a type that does not say which call produced it.
232
+ exempt: [operations whose request carries no fields and is not modelled at all]
233
+
234
+ - id: STRUCT-04
235
+ title: A screen owns an analytics surface
236
+ severity: important
237
+ enforcement: lint
238
+ predicate: file_required_in_dir
239
+ params:
240
+ role: screen.analytics
241
+ screen_exemptable: true
242
+ rationale: observability
243
+ check: >
244
+ A screen a user can reach reports that they reached it. The surface is one type per screen so
245
+ that its events can be spied in a test without touching the tracker.
246
+ exempt: >
247
+ Screens with no product identity of their own - a picker sheet, a system-wrapper cover, a web
248
+ host - presented inside another screen's flow. The presenting screen tracks the interaction
249
+ that opened them. Inventing screen-view events for them puts names in the analytics schema
250
+ nobody asked for. List them in the overlay's `exempt_screens`.
251
+
252
+ - id: STRUCT-05
253
+ title: A screen ships the construction seam its module's assembly shape declares
254
+ severity: important
255
+ enforcement: scan
256
+ predicate: file_required_in_dir
257
+ params:
258
+ slot: ScreenAssemblyShape
259
+ slot_value: per-screen-factory
260
+ role: screen.factory
261
+ applies_when: the module binds ScreenAssemblyShape to per-screen-factory. Otherwise DISABLED.
262
+ rationale: module boundaries
263
+ check: >
264
+ Under per-screen-factory, each screen ships its own construction seam. Under shared-factory
265
+ the seam is one per module and this rule does not run per screen.
266
+
267
+ - id: STRUCT-06
268
+ title: UI state lives where the module decided it lives
269
+ severity: important
270
+ enforcement: lint
271
+ predicate: file_required_in_dir
272
+ params:
273
+ slot: UIStateHolder
274
+ slot_value: separate-state-type
275
+ role: screen.state
276
+ trigger_role: screen.viewmodel
277
+ trigger_pattern: '(FormField|\.Section\b|var\s+(is|selected|shows|expanded)\w*(Presented|Expanded|Sheet|Index|Visible|Selected|Shown)\b)'
278
+ applies_when: >
279
+ the module binds UIStateHolder to separate-state-type AND the screen actually has UI state
280
+ (a bound form field, a selected index, a presented sheet). Otherwise DISABLED.
281
+ rationale: testability
282
+ check: >
283
+ A screen with bound form objects or visual state, in a module that separated that state out,
284
+ must carry the state type. A screen with neither is not missing anything.
285
+
286
+ - id: STRUCT-07
287
+ title: An extracted view carries the value type it renders
288
+ severity: important
289
+ enforcement: lint
290
+ predicate: sibling_required
291
+ params:
292
+ slot: SubviewShape
293
+ slot_value: folder-per-subview
294
+ subject_role: subview.view
295
+ strip_suffix_from_vocabulary: SubviewViewSuffix
296
+ append_suffix_from_vocabulary: SubviewConfigurationSuffix
297
+ applies_when: the module binds SubviewShape to folder-per-subview. Otherwise DISABLED.
298
+ rationale: testability
299
+ check: >
300
+ Every extracted view has a value type beside it holding what it renders. A view that reads
301
+ its data from anywhere else cannot be rendered in isolation, which means it cannot be
302
+ previewed or snapshotted.
303
+
304
+ - id: STRUCT-08
305
+ title: The value type an extracted view renders holds values, not behaviour
306
+ severity: blocking
307
+ enforcement: lint
308
+ predicate: forbidden_pattern
309
+ params:
310
+ slot: SubviewShape
311
+ slot_value: folder-per-subview
312
+ subject_role: subview.configuration
313
+ pattern: '^\s+(let|var)\s+\w+\s*:\s*(@escaping\s+)?\('
314
+ applies_when: the module binds SubviewShape to folder-per-subview. Otherwise DISABLED.
315
+ rationale: testability
316
+ check: >
317
+ A closure stored in the value type makes it non-comparable and drags the caller's lifetime
318
+ into it. Actions reach the view as its own parameters; the value type carries only what is
319
+ drawn.
320
+
321
+ - id: STRUCT-09
322
+ title: An extracted view takes values, never the screen's view model
323
+ severity: blocking
324
+ enforcement: lint
325
+ predicate: forbidden_pattern
326
+ params:
327
+ slot: SubviewShape
328
+ subject_role: subview.view
329
+ pattern: '^\s+(let|var)\s+\w+\s*:\s*(any\s+)?\w*ViewModel\b'
330
+ applies_when: the module binds SubviewShape. Otherwise DISABLED.
331
+ rationale: testability
332
+ check: >
333
+ A view holding the view model can reach anything, so nothing about it can be asserted from
334
+ its inputs. It also cannot be previewed without constructing the whole screen.
335
+
336
+ - id: STRUCT-10
337
+ title: The screen's entry file composes; it does not also carry the pieces
338
+ severity: important
339
+ applies_when: the module binds ScreenCompositionShape to extracted. Otherwise DISABLED.
340
+ enforcement: lint
341
+ predicate: forbidden_member
342
+ params:
343
+ slot: ScreenCompositionShape
344
+ slot_value: extracted
345
+ subject_role: screen.entry
346
+ pattern: '^\s+(?:@ViewBuilder\s+)?(?:private\s+)?(?:var|func)\s+(\w+)[^\n]*?some View'
347
+ allow: [body]
348
+ rationale: readability
349
+ check: >
350
+ The entry file holds its properties, its init, one body and its previews. A second view
351
+ member on it is a fragment that either belongs in the body's composition or belongs beside
352
+ the screen as an extracted view. The exception is a fragment the type system pins to the call
353
+ site - an alert's content, a modifier-constrained builder - which cannot be moved.
354
+
355
+ - id: STRUCT-11
356
+ title: A cross-screen folder holds no type named after one screen
357
+ severity: important
358
+ enforcement: lint
359
+ predicate: prefix_collision
360
+ params:
361
+ subject_role: shared.root
362
+ rationale: module boundaries
363
+ check: >
364
+ A type in the shared folder carrying a screen's prefix is shared by accident. Either it is
365
+ genuinely common, and the prefix is wrong, or it belongs to that screen and the folder is
366
+ wrong.
367
+
368
+ - id: STRUCT-12
369
+ title: The test tree mirrors the source tree
370
+ severity: suggestion
371
+ enforcement: scan
372
+ predicate: mirror_required
373
+ params:
374
+ test_role: test.root
375
+ source_role: source.root
376
+ rationale: navigability
377
+ check: >
378
+ A reader looking for a type's tests should find them at the same path under the test root. A
379
+ test folder with no source counterpart is either testing something that moved or grouping by
380
+ a concept the source does not have.
381
+
382
+ # --- Size and shape ------------------------------------------------------
383
+ - id: STRUCT-13
384
+ title: File size has a target and a ceiling, and tests get a wider one
385
+ severity: important
386
+ enforcement: lint
387
+ predicate: file_size
388
+ params:
389
+ test_marker: /Tests/
390
+ rationale: readability
391
+ metric: lines per file
392
+ check: >
393
+ Past the target a file is a candidate for splitting; past the ceiling it is a finding. Tests
394
+ get a wider band because a suite legitimately repeats its arrange step. The numbers live in
395
+ the overlay - a module that has not chosen them gets the registry defaults.
396
+ exempt: [generated sources, fixture and mock data files]
397
+
398
+ - id: STRUCT-14
399
+ title: Layout numbers are not a type
400
+ severity: suggestion
401
+ enforcement: lint
402
+ predicate: forbidden_pattern
403
+ params:
404
+ glob_from_vocabulary: PresentationDir
405
+ pattern: '^\s*(?:private\s+)?(?:struct|enum)\s+\w+(Constants|Layout|Metrics|Dimensions)\b'
406
+ rationale: readability
407
+ check: >
408
+ A per-screen constants, layout or metrics type collects numbers that belong either to a
409
+ design-token namespace or to the one view that uses them. It becomes a dumping ground whose
410
+ entries no one dares delete.
411
+
412
+ - id: STRUCT-15
413
+ title: A view type is previewable, and previews it
414
+ severity: suggestion
415
+ enforcement: lint
416
+ predicate: required_pattern
417
+ params:
418
+ glob: '**/*.swift'
419
+ when_pattern: '^\s*(?:public\s+)?struct\s+\w+(View|Scene)\s*:'
420
+ pattern: '#Preview'
421
+ label: no preview
422
+ rationale: testability
423
+ check: >
424
+ A view with no preview is a view nobody looked at in isolation. The preview is also the
425
+ cheapest proof that the type can be constructed from values alone.
426
+
427
+ # --- Vocabulary ----------------------------------------------------------
428
+ - id: VOCAB-01
429
+ title: A boolean reads as a claim about the subject
430
+ severity: important
431
+ enforcement: lint
432
+ predicate: naming_pattern
433
+ params:
434
+ glob: '**/*.swift'
435
+ declaration: '^\s+(?:@\w+\s+)?(?:private\(set\)\s+)?(?:public\s+)?var\s+([a-z]\w*)\s*:\s*Bool\b'
436
+ accept_from_vocabulary: BooleanPrefixes
437
+ rationale: readability
438
+ check: >
439
+ A boolean is prefixed so the reader knows it is one and knows which way it points. Plural
440
+ subjects agree with the plural rather than taking a singular prefix. A bare adjective or noun
441
+ forces the reader to open the declaration.
442
+
443
+ - id: VOCAB-02
444
+ title: A positional index carries the same label everywhere
445
+ severity: suggestion
446
+ enforcement: lint
447
+ predicate: naming_pattern
448
+ params:
449
+ glob: '**/*.swift'
450
+ declaration: '\((for|_)\s+(?:index|\w+Index)\s*:\s*Int'
451
+ accept_from_vocabulary: IndexLabel
452
+ rationale: readability
453
+ check: >
454
+ One spelling for "at this position" across the module. Two spellings make the call sites read
455
+ as two different concepts.
456
+
457
+ - id: VOCAB-03
458
+ title: Section headings come from a closed vocabulary
459
+ severity: suggestion
460
+ enforcement: lint
461
+ predicate: vocabulary
462
+ params:
463
+ vocabulary_key: SectionHeadings
464
+ declaration: '^\s*//\s*MARK:\s*-\s*(.+)$'
465
+ exempt_patterns: ['^[a-z][a-zA-Z0-9/{}.\- ]*$', '→', '↔']
466
+ rationale: navigability
467
+ check: >
468
+ Headings are a map, and a map with a hundred distinct labels is not one. The vocabulary lives
469
+ in the overlay. Headings naming an external contract - an endpoint, a protocol being
470
+ conformed to, a wire direction - are outside the vocabulary by construction and are exempt.
471
+
472
+ - id: VOCAB-04
473
+ title: A method name says which layer speaks
474
+ severity: important
475
+ enforcement: lint
476
+ predicate: naming_pattern
477
+ params:
478
+ subject_role: screen.viewmodel
479
+ declaration: '^\s+(?:private\s+)?func\s+([a-z]\w*)\s*\('
480
+ reject_from_vocabulary: BareVerbs
481
+ rationale: readability
482
+ check: >
483
+ A user event, a service call and a derived value are three different things and their names
484
+ say which. A bare verb on a presentation type does not say whether it asks the network, mutates
485
+ state, or both.
486
+
487
+ - id: VOCAB-05
488
+ title: A type's last word says what it does, and some last words say nothing
489
+ severity: important
490
+ enforcement: lint
491
+ predicate: naming_pattern
492
+ params:
493
+ glob: '**/*.swift'
494
+ declaration: '^(?:public\s+)?(?:final\s+)?(?:struct|class|enum|actor|protocol)\s+(\w+)'
495
+ reject_from_vocabulary: ForbiddenTypeSuffixes
496
+ rationale: readability
497
+ check: >
498
+ The role suffix set lives in the overlay. Suffixes that describe no role - the ones that mean
499
+ "code that does things" - are listed there as forbidden, and a type carrying one has not been
500
+ named yet.
501
+
502
+ - id: VOCAB-06
503
+ title: Copy is resolved in one place per module
504
+ severity: suggestion
505
+ enforcement: lint
506
+ predicate: forbidden_pattern
507
+ params:
508
+ slot: CopyResolution
509
+ glob: '**/*.swift'
510
+ pattern_from_vocabulary_by_slot:
511
+ render-site: CopyLayerTypePattern
512
+ copy-layer: RawCopyKeyAtRenderSitePattern
513
+ applies_when: the module binds CopyResolution. Unbound, this rule is DISABLED.
514
+ rationale: navigability
515
+ check: >
516
+ Under `render-site` a per-screen copy layer is the finding; under `copy-layer` a raw key at a
517
+ render site is. Either is fine; both at once means a reader has to check two places.
518
+
519
+ - id: VOCAB-07
520
+ title: One declaration per file, and the file is named after it
521
+ severity: important
522
+ enforcement: lint
523
+ predicate: naming_pattern
524
+ params:
525
+ glob: '**/*.swift'
526
+ declaration: '^(?:public\s+)?(?:final\s+)?(?:struct|class|enum|actor|protocol)\s+(\w+)'
527
+ match_file_stem: true
528
+ rationale: navigability
529
+ check: >
530
+ A reader who knows a type's name knows its file. A second top-level declaration in a file is
531
+ findable only by grep - and gets deleted by accident when the first one moves.
532
+ exempt: >
533
+ A private helper that exists only for the file's own declaration, and an extension of the
534
+ file's own type.
535
+
536
+ # --- Judgement (surfaced, never auto-failed) -----------------------------
537
+ - id: STRUCT-16
538
+ title: A subfolder is earned by its contents
539
+ severity: suggestion
540
+ enforcement: judgement
541
+ predicate: none
542
+ params: {}
543
+ rationale: navigability
544
+ check: >
545
+ A folder holding one or two files usually names a concept the module does not actually have.
546
+ The tool can list the candidates; whether a given one is a seam worth keeping is a reading,
547
+ not a count. Service operation folders are exempt by construction - the pair is the point.
548
+
549
+ - id: STRUCT-17
550
+ title: Fixture data lives with the fixtures, not inside the thing that serves it
551
+ severity: suggestion
552
+ enforcement: judgement
553
+ predicate: none
554
+ params: {}
555
+ rationale: readability
556
+ check: >
557
+ A scripted implementation that also carries its own payload literals mixes the decision of
558
+ which scenario to answer with the content of the answer. The tool can surface the size ratio;
559
+ the split itself is a judgement.