@mmerterden/multi-agent-pipeline 14.2.2 → 15.0.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 (122) hide show
  1. package/CHANGELOG.md +76 -6
  2. package/README.md +15 -8
  3. package/README.tr.md +15 -8
  4. package/docs/FIGMA_PIPELINE.md +3 -3
  5. package/docs/adr/0006-skills-core-external-split.md +1 -1
  6. package/docs/adr/0009-claude-stack-skills-plugin-only.md +31 -0
  7. package/docs/adr/README.md +1 -0
  8. package/docs/architecture.md +7 -7
  9. package/docs/ecosystem.md +28 -28
  10. package/docs/features.md +5 -5
  11. package/index.js +2 -0
  12. package/install/_codex-agents.mjs +11 -2
  13. package/install/_common.mjs +65 -1
  14. package/install/_dev-only-files.mjs +0 -1
  15. package/install/_platform-filter.mjs +73 -7
  16. package/install/_plugin-skills.mjs +19 -8
  17. package/install/claude.mjs +144 -59
  18. package/install/codex.mjs +28 -3
  19. package/install/copilot.mjs +36 -11
  20. package/install/index.mjs +6 -2
  21. package/install/templates/codex-instructions.md +1 -1
  22. package/install/templates/copilot-instructions.md +3 -3
  23. package/package.json +1 -2
  24. package/pipeline/commands/multi-agent/SKILL.md +2 -0
  25. package/pipeline/commands/multi-agent/analysis/SKILL.md +3 -3
  26. package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
  27. package/pipeline/commands/multi-agent/build-optimize/SKILL.md +9 -9
  28. package/pipeline/commands/multi-agent/channels/SKILL.md +1 -1
  29. package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +186 -0
  30. package/pipeline/commands/multi-agent/dev/SKILL.md +1 -1
  31. package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +1 -1
  32. package/pipeline/commands/multi-agent/dev-local/SKILL.md +1 -1
  33. package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +1 -1
  34. package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
  35. package/pipeline/commands/multi-agent/help/SKILL.md +19 -4
  36. package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +2 -2
  37. package/pipeline/commands/multi-agent/jira/SKILL.md +1 -1
  38. package/pipeline/commands/multi-agent/prune-prompts/SKILL.md +81 -0
  39. package/pipeline/commands/multi-agent/resume/SKILL.md +1 -1
  40. package/pipeline/commands/multi-agent/{ship → resume-local}/SKILL.md +8 -8
  41. package/pipeline/commands/multi-agent/setup/SKILL.md +5 -5
  42. package/pipeline/commands/multi-agent/stack/SKILL.md +55 -43
  43. package/pipeline/commands/multi-agent/store-ready/SKILL.md +3 -3
  44. package/pipeline/commands/multi-agent/sync/SKILL.md +18 -11
  45. package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +1 -1
  46. package/pipeline/commands/multi-agent/uninstall/SKILL.md +2 -0
  47. package/pipeline/commands/multi-agent/update/SKILL.md +1 -1
  48. package/pipeline/lib/issue-fetcher.sh +1 -1
  49. package/pipeline/lib/parse-complaints.sh +306 -0
  50. package/pipeline/multi-agent-refs/channels/wiki.md +3 -3
  51. package/pipeline/multi-agent-refs/complaint-analysis-template.md +99 -0
  52. package/pipeline/multi-agent-refs/component-dispatch.md +6 -6
  53. package/pipeline/multi-agent-refs/cross-cli-contract.md +16 -16
  54. package/pipeline/multi-agent-refs/features/external-context-injection.md +1 -1
  55. package/pipeline/multi-agent-refs/features/stack-skill-routing.md +5 -5
  56. package/pipeline/multi-agent-refs/generate-issue.md +1 -1
  57. package/pipeline/multi-agent-refs/phases/modes.md +1 -1
  58. package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
  59. package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +7 -7
  60. package/pipeline/multi-agent-refs/phases/phase-2-planning.md +5 -5
  61. package/pipeline/multi-agent-refs/phases/phase-3-dev.md +3 -3
  62. package/pipeline/multi-agent-refs/phases/phase-4-review.md +12 -12
  63. package/pipeline/multi-agent-refs/phases/phase-5-test.md +1 -1
  64. package/pipeline/multi-agent-refs/tracker-contract.md +1 -1
  65. package/pipeline/multi-agent-refs/wiki-capture.md +2 -2
  66. package/pipeline/preferences-template.json +13 -5
  67. package/pipeline/rules/figma-pipeline.md +2 -2
  68. package/pipeline/schemas/agent-state.schema.json +1 -1
  69. package/pipeline/schemas/complaint-analysis-spec.schema.json +216 -0
  70. package/pipeline/schemas/migrations/prefs-2.5.0-to-2.6.0.mjs +46 -0
  71. package/pipeline/schemas/prefs.schema.json +276 -66
  72. package/pipeline/schemas/token-budget.json +2 -2
  73. package/pipeline/scripts/_stack-routing.mjs +79 -0
  74. package/pipeline/scripts/audit-log-rotate.sh +4 -1
  75. package/pipeline/scripts/build-skills-index.mjs +11 -0
  76. package/pipeline/scripts/build-stack-plugins.mjs +28 -60
  77. package/pipeline/scripts/check-derived-drift.mjs +52 -28
  78. package/pipeline/scripts/gc-worktrees.sh +4 -1
  79. package/pipeline/scripts/gen-skills-index.mjs +1 -1
  80. package/pipeline/scripts/match-skills.mjs +8 -2
  81. package/pipeline/scripts/migrate-prefs.mjs +28 -20
  82. package/pipeline/scripts/phase-tracker.sh +13 -5
  83. package/pipeline/scripts/phase0-exit-gate.mjs +3 -2
  84. package/pipeline/scripts/run-aggregator.mjs +7 -2
  85. package/pipeline/scripts/scan-agent-config.sh +1 -1
  86. package/pipeline/scripts/skill-conformance.mjs +165 -30
  87. package/pipeline/scripts/smoke-cross-cli-behavior.sh +1 -1
  88. package/pipeline/scripts/test-gap-rules/android.json +25 -0
  89. package/pipeline/scripts/test-gap-rules/ios.json +34 -0
  90. package/pipeline/scripts/test-gap-rules/node.json +29 -0
  91. package/pipeline/scripts/test-gap-rules/python.json +25 -0
  92. package/pipeline/scripts/uninstall.mjs +158 -11
  93. package/pipeline/scripts/validate-complaint-doc.mjs +229 -0
  94. package/pipeline/scripts/validate-reviewer.mjs +9 -3
  95. package/pipeline/skills/.skill-manifest.json +156 -108
  96. package/pipeline/skills/.skills-index.json +449 -12
  97. package/pipeline/skills/shared/README.md +14 -10
  98. package/pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md +1 -1
  99. package/pipeline/skills/shared/core/multi-agent-build-optimize/SKILL.md +1 -1
  100. package/pipeline/skills/shared/core/multi-agent-complaint-analysis/SKILL.md +49 -0
  101. package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +1 -1
  102. package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +1 -1
  103. package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +1 -1
  104. package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +1 -1
  105. package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +2 -2
  106. package/pipeline/skills/shared/core/multi-agent-prune-prompts/SKILL.md +83 -0
  107. package/pipeline/skills/shared/core/{multi-agent-ship → multi-agent-resume-local}/SKILL.md +6 -6
  108. package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +79 -22
  109. package/pipeline/skills/shared/core/multi-agent-store-ready/SKILL.md +1 -1
  110. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +8 -8
  111. package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +1 -1
  112. package/pipeline/skills/shared/external/ios-coding-standard/modules/_TEMPLATE.yml +2 -2
  113. package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +368 -33
  114. package/pipeline/skills/shared/external/ios-coding-standard/references/swiftlint.draft.yml +1 -2
  115. package/pipeline/skills/shared/external/ios-coding-standard/scripts/check_structure.py +765 -0
  116. package/pipeline/skills/shared/external/ios-module-structure/SKILL.md +75 -0
  117. package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +131 -0
  118. package/pipeline/skills/shared/external/ios-module-structure/references/rules.yml +559 -0
  119. package/pipeline/skills/shared/external/ios-module-structure/scripts/check_structure.py +765 -0
  120. package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +53 -10
  121. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +4 -3
  122. package/pipeline/skills/skills-index.md +7 -4
@@ -36,6 +36,31 @@ enforcement_kinds:
36
36
  judgement: requires a human or an audit run
37
37
  exception_marker: "// standard:exception(<RULE-ID>) <reason> <expiry:YYYY-MM-DD>"
38
38
 
39
+ # ---------------------------------------------------------------------------
40
+ # Roles - what the structure rules name instead of paths
41
+ # ---------------------------------------------------------------------------
42
+ # A structure rule never names a folder or a type; it names a role, and the module's overlay binds
43
+ # that role to its own spelling. An UNBOUND role disables every rule that reads it, and the audit
44
+ # reports that rather than guessing a shape. Substitutions available in a binding: {dir}, {stem}.
45
+ roles:
46
+ screen.root: the directory that is one screen; everything else resolves relative to it
47
+ screen.entry: the file a coordinator or factory constructs to show the screen
48
+ screen.viewmodel: the type holding the screen's behaviour and service calls
49
+ screen.state: the type holding UI state, when the module separates it from the view model
50
+ screen.analytics: the screen's analytics surface
51
+ screen.factory: the seam another module calls to build the screen
52
+ screen.mapper: the wire-to-domain translation for the screen
53
+ service.dir: one directory per service operation, holding that operation's models
54
+ service.request: the request half of a service operation
55
+ service.response: the response half of a service operation
56
+ subview.view: an extracted view belonging to one screen
57
+ subview.configuration: the value type an extracted view renders
58
+ repository.live: the production implementation of a screen's data access
59
+ repository.mock: the scripted implementation previews and the debug menu use
60
+ shared.root: the module's cross-screen folder
61
+ source.root: the module's source tree, as the mirror rule's counterpart to the test tree
62
+ test.root: the module's test tree
63
+
39
64
  module_overlay_slots:
40
65
  description: >
41
66
  Some rules govern a CHOICE rather than a defect: two shapes are each internally coherent, the
@@ -48,6 +73,45 @@ module_overlay_slots:
48
73
  counter-example: a module with no copy surface has not chosen a different dialect, it is
49
74
  missing the surface, so that rule is not slotted.
50
75
  slots:
76
+ - id: ScreenLayerShape
77
+ governs: [STRUCT-02, STRUCT-03]
78
+ values:
79
+ layered: every screen carries the same layer folders, including the ones empty for it.
80
+ organic: a screen carries only the layers it actually has.
81
+ note: Decides whether "this screen has no data layer" is a finding or a fact.
82
+ - id: ServiceModelDir
83
+ governs: [STRUCT-09]
84
+ values:
85
+ under-data: the request/response pair sits in the data layer, beside the calling code.
86
+ under-mapper: the pair sits beside the mapper that translates it.
87
+ - id: ServiceModelPairing
88
+ governs: [STRUCT-09]
89
+ values:
90
+ both: every operation models a request and a response, even when the request is empty.
91
+ response-only: a request is modelled only when the call carries a body.
92
+ - id: ScreenAssemblyShape
93
+ governs: [STRUCT-10]
94
+ values:
95
+ per-screen-factory: each screen ships its own construction seam.
96
+ shared-factory: one factory per module builds every screen.
97
+ - id: UIStateHolder
98
+ governs: [STRUCT-11]
99
+ values:
100
+ separate-state-type: UI state lives in its own type beside the view model.
101
+ view-model-owned: the view model holds UI state directly.
102
+ - id: SubviewShape
103
+ governs: [READ-04b, STRUCT-12, STRUCT-13]
104
+ values:
105
+ folder-per-subview: each extracted view gets a folder holding it and its value type.
106
+ flat: extracted views sit loose in one folder, with no required companion.
107
+ - id: ScreenCompositionShape
108
+ governs: [STRUCT-07]
109
+ values:
110
+ extracted: a fragment of the screen becomes a named view beside it.
111
+ in-file: the entry file composes its own fragments.
112
+ note: >
113
+ Both keep the body readable, which is what STRUCT-07 protects. Counting view members on
114
+ entry files against extracted view files tells you which one the module chose.
51
115
  - id: ServiceNamingScheme
52
116
  governs: [SVC-07]
53
117
  values:
@@ -161,12 +225,16 @@ sensitive_data_classes:
161
225
  loggable: never
162
226
 
163
227
  rules:
164
-
165
228
  # ── READ - format & readability ───────────────────────────────────────────
166
229
  - id: READ-01
167
230
  title: MARK sections separate concerns - business rules, service calls, UI, one group each
168
231
  severity: important
169
- enforcement: judgement
232
+ predicate: vocabulary
233
+ params:
234
+ vocabulary_key: SectionHeadings
235
+ declaration: '^\s*//\s*MARK:\s*-\s*(.+)$'
236
+ exempt_patterns: ['^[a-z][a-zA-Z0-9/{}.\- ]*$', '→', '↔']
237
+ enforcement: lint
170
238
  rationale: readability
171
239
  check: >
172
240
  A file mixing a service call, a business rule and a view fragment under one MARK (or none)
@@ -179,7 +247,7 @@ rules:
179
247
  title: Comments are clear statements; no AI or tool attribution anywhere
180
248
  severity: blocking
181
249
  enforcement: lint
182
- mechanism: 'custom regex: (?i)(generated by|copilot|co-authored-by:.*(claude|anthropic|ai))'
250
+ mechanism: "custom regex: (?i)(generated by|copilot|co-authored-by:.*(claude|anthropic|ai))"
183
251
  rationale: readability
184
252
  check: >
185
253
  Trim a comment by deleting what the code already says, the design-frame archaeology and the
@@ -194,6 +262,9 @@ rules:
194
262
  - id: READ-03
195
263
  title: Size thresholds with carve-outs - screen 600 / file 400 / function 40 lines
196
264
  severity: important
265
+ predicate: file_size
266
+ params:
267
+ test_marker: /Tests/
197
268
  enforcement: lint
198
269
  mechanism: swiftlint file_length, function_body_length, type_body_length
199
270
  rationale: readability
@@ -228,7 +299,14 @@ rules:
228
299
  - id: READ-04b
229
300
  title: A view fragment that renders a thing is a component file with a preview
230
301
  severity: important
231
- enforcement: judgement
302
+ predicate: sibling_required
303
+ params:
304
+ slot: SubviewShape
305
+ slot_value: folder-per-subview
306
+ subject_role: subview.view
307
+ strip_suffix_from_vocabulary: SubviewViewSuffix
308
+ append_suffix_from_vocabulary: SubviewConfigurationSuffix
309
+ enforcement: lint
232
310
  rationale: testability
233
311
  check: >
234
312
  A `private var x: some View` inside a scene is a component in disguise. If it renders a
@@ -316,6 +394,11 @@ rules:
316
394
  - id: READ-07
317
395
  title: Casing, boolean prefixes, guard-early / return-early
318
396
  severity: suggestion
397
+ predicate: naming_pattern
398
+ params:
399
+ glob: '**/*.swift'
400
+ declaration: '^\s+(?:@\w+\s+)?(?:private\(set\)\s+)?(?:public\s+)?var\s+([a-z]\w*)\s*:\s*Bool\b'
401
+ accept_from_vocabulary: BooleanPrefixes
319
402
  enforcement: lint
320
403
  mechanism: swiftlint identifier_name, type_name, cyclomatic_complexity
321
404
  rationale: readability
@@ -342,7 +425,12 @@ rules:
342
425
  - id: STRUCT-01
343
426
  title: One primary top-level type per file; nesting only for owned details
344
427
  severity: important
345
- enforcement: judgement
428
+ predicate: naming_pattern
429
+ params:
430
+ glob: '**/*.swift'
431
+ declaration: '^(?:public\s+)?(?:final\s+)?(?:struct|class|enum|actor|protocol)\s+(\w+)'
432
+ match_file_stem: true
433
+ enforcement: lint
346
434
  rationale: readability
347
435
  check: >
348
436
  Forbidden nested: entity, domain model, request/response payload, list element. Allowed
@@ -358,7 +446,12 @@ rules:
358
446
  - id: STRUCT-02
359
447
  title: A screen is a known file manifest, not a pile
360
448
  severity: important
361
- enforcement: judgement
449
+ predicate: dir_required_in_dir
450
+ params:
451
+ slot: ScreenLayerShape
452
+ slot_value: layered
453
+ vocabulary_key: LayerDirs
454
+ enforcement: lint
362
455
  rationale: readability
363
456
  check: >
364
457
  Scene, ViewModel, LocalizedText, CoordinatorEvent, AnalyticsTracking, UseCase, Repository
@@ -368,19 +461,30 @@ rules:
368
461
  - id: STRUCT-03
369
462
  title: Every screen sits at the same depth with the same internal grouping
370
463
  severity: important
371
- enforcement: judgement
464
+ predicate: dir_required_in_dir
465
+ params:
466
+ vocabulary_key: PresentationDir
467
+ enforcement: lint
372
468
  rationale: readability
373
469
 
374
470
  - id: STRUCT-04
375
471
  title: Nothing lives outside the layout - no loose root type, no Utils/Helpers/Misc bucket
376
472
  severity: important
377
- enforcement: judgement
473
+ predicate: naming_pattern
474
+ params:
475
+ glob: '**/*.swift'
476
+ declaration: '^(?:public\s+)?(?:final\s+)?(?:struct|class|enum|actor|protocol)\s+(\w+)'
477
+ reject_from_vocabulary: ForbiddenTypeSuffixes
478
+ enforcement: lint
378
479
  rationale: readability
379
480
 
380
481
  - id: STRUCT-05
381
482
  title: Type placement follows a consumer-count ladder
382
483
  severity: important
383
- enforcement: judgement
484
+ predicate: prefix_collision
485
+ params:
486
+ subject_role: shared.root
487
+ enforcement: lint
384
488
  rationale: flexibility
385
489
  check: >
386
490
  2+ modules -> cross-module shared tier. 2+ screens -> module shared entities. One screen ->
@@ -499,7 +603,14 @@ rules:
499
603
  - id: STRUCT-07
500
604
  title: A scene is one type - no inner view struct wrapping it
501
605
  severity: important
502
- enforcement: judgement
606
+ predicate: forbidden_member
607
+ params:
608
+ slot: ScreenCompositionShape
609
+ slot_value: extracted
610
+ subject_role: screen.entry
611
+ pattern: '^\s+(?:@ViewBuilder\s+)?(?:private\s+)?(?:var|func)\s+(\w+)[^\n]*?some View'
612
+ allow: [body]
613
+ enforcement: lint
503
614
  rationale: readability
504
615
  check: >
505
616
  `struct XScene: View { var body: some View { XView(viewModel: viewModel) } }` with the real
@@ -882,7 +993,7 @@ rules:
882
993
  title: Assets come from the app's own CDN
883
994
  severity: blocking
884
995
  enforcement: lint
885
- mechanism: 'custom regex: https?:// host not on the app''s own domains, in a non-test source'
996
+ mechanism: "custom regex: https?:// host not on the app's own domains, in a non-test source"
886
997
  rationale: security
887
998
  check: >
888
999
  An image or asset URL pointing at a third-party host is a finding even when it renders
@@ -896,14 +1007,14 @@ rules:
896
1007
  title: Escaping closures capture self weakly and unwrap immediately
897
1008
  severity: important
898
1009
  enforcement: lint
899
- mechanism: 'custom regex: escaping closure body referencing self without a weak capture list'
1010
+ mechanism: "custom regex: escaping closure body referencing self without a weak capture list"
900
1011
  rationale: security
901
1012
 
902
1013
  - id: SAFE-02
903
1014
  title: A calendar day sent to a service uses the shared calendar-day helper
904
1015
  severity: blocking
905
1016
  enforcement: lint
906
- mechanism: 'custom regex: DateFormatter constructed in a mapper or repository path'
1017
+ mechanism: "custom regex: DateFormatter constructed in a mapper or repository path"
907
1018
  rationale: security
908
1019
 
909
1020
  # ── VIS - declaration & visibility ────────────────────────────────────────
@@ -938,7 +1049,19 @@ rules:
938
1049
 
939
1050
  # ── UI ────────────────────────────────────────────────────────────────────
940
1051
  - id: UI-01
941
- scope: { frameworks: [swiftui], paths: ["**/*View.swift", "**/*Screen.swift", "**/*Scene.swift", "**/*Cell.swift", "**/*Configuration.swift", "**/*+Modifiers.swift"] }
1052
+ scope:
1053
+ {
1054
+ frameworks: [swiftui],
1055
+ paths:
1056
+ [
1057
+ "**/*View.swift",
1058
+ "**/*Screen.swift",
1059
+ "**/*Scene.swift",
1060
+ "**/*Cell.swift",
1061
+ "**/*Configuration.swift",
1062
+ "**/*+Modifiers.swift",
1063
+ ],
1064
+ }
942
1065
  scope_reason: SwiftUI view composition - a UIKit view controller hierarchy expresses reuse differently
943
1066
  title: A screen's composite view is never consumed by another screen
944
1067
  severity: blocking
@@ -946,7 +1069,19 @@ rules:
946
1069
  rationale: flexibility
947
1070
 
948
1071
  - id: UI-02
949
- scope: { frameworks: [swiftui], paths: ["**/*View.swift", "**/*Screen.swift", "**/*Scene.swift", "**/*Cell.swift", "**/*Configuration.swift", "**/*+Modifiers.swift"] }
1072
+ scope:
1073
+ {
1074
+ frameworks: [swiftui],
1075
+ paths:
1076
+ [
1077
+ "**/*View.swift",
1078
+ "**/*Screen.swift",
1079
+ "**/*Scene.swift",
1080
+ "**/*Cell.swift",
1081
+ "**/*Configuration.swift",
1082
+ "**/*+Modifiers.swift",
1083
+ ],
1084
+ }
950
1085
  scope_reason: SwiftUI view construction
951
1086
  title: No new component is added to a UI target the module marks as frozen
952
1087
  severity: important
@@ -955,28 +1090,64 @@ rules:
955
1090
  rationale: flexibility
956
1091
 
957
1092
  - id: UI-03
958
- scope: { frameworks: [swiftui], paths: ["**/*View.swift", "**/*Screen.swift", "**/*Scene.swift", "**/*Cell.swift", "**/*Configuration.swift", "**/*+Modifiers.swift"] }
1093
+ scope:
1094
+ {
1095
+ frameworks: [swiftui],
1096
+ paths:
1097
+ [
1098
+ "**/*View.swift",
1099
+ "**/*Screen.swift",
1100
+ "**/*Scene.swift",
1101
+ "**/*Cell.swift",
1102
+ "**/*Configuration.swift",
1103
+ "**/*+Modifiers.swift",
1104
+ ],
1105
+ }
959
1106
  scope_reason: SwiftUI view construction
960
1107
  title: Every screen with user actions has its analytics surface; no direct generated-event calls
961
1108
  severity: important
962
- enforcement: judgement
1109
+ predicate: file_required_in_dir
1110
+ params:
1111
+ role: screen.analytics
1112
+ enforcement: lint
963
1113
  rationale: readability
964
1114
 
965
1115
  - id: UI-04
966
- scope: { frameworks: [swiftui], paths: ["**/*View.swift", "**/*Screen.swift", "**/*Scene.swift", "**/*Cell.swift", "**/*Configuration.swift", "**/*+Modifiers.swift"] }
1116
+ scope:
1117
+ {
1118
+ frameworks: [swiftui],
1119
+ paths:
1120
+ [
1121
+ "**/*View.swift",
1122
+ "**/*Screen.swift",
1123
+ "**/*Scene.swift",
1124
+ "**/*Cell.swift",
1125
+ "**/*Configuration.swift",
1126
+ "**/*+Modifiers.swift",
1127
+ ],
1128
+ }
967
1129
  scope_reason: SwiftUI copy surface (LocalizedText); UIKit modules localise through a different seam
968
1130
  title: All user copy goes through the screen's copy surface
969
1131
  severity: important
970
- enforcement: lint
971
- mechanism: 'custom regex: raw localization key access inside Scene/ViewModel'
1132
+ enforcement: judgement
1133
+ mechanism: "custom regex: raw localization key access inside Scene/ViewModel"
972
1134
  rationale: flexibility
973
1135
 
974
1136
  # ── MOD - modularity ──────────────────────────────────────────────────────
1137
+ conflict: >
1138
+ The module-structure pass that produced STRUCT-09..14 came from a folder-structure document
1139
+ that says the opposite - copy is resolved AT THE RENDER SITE, and a per-screen copy type is
1140
+ the finding. One module has already been refactored to that shape and now has zero copy
1141
+ surfaces, which this rule reads as a violation on every screen. Two documents the same team
1142
+ owns disagree; until one is retired, this rule is not mechanically enforced. Do not resolve
1143
+ it by binding a slot: this registry's own slot doctrine names UI-04 as the counter-example -
1144
+ a module with no copy surface has not chosen a dialect, it is missing the surface.
1145
+
975
1146
  - id: MOD-01
976
1147
  title: A feature module never imports another feature module
977
1148
  severity: blocking
978
1149
  enforcement: lint
979
- mechanism: 'custom regex per module: ^import (<sibling feature names>)'
1150
+ mechanism: "custom regex per module: ^import (<sibling feature names>)"
980
1151
  rationale: flexibility
981
1152
  check: A feature that imports many siblings to compose them has promoted itself to a second composition root - highest severity in this section.
982
1153
 
@@ -1050,7 +1221,7 @@ rules:
1050
1221
  title: One concurrency model - no GCD, semaphore or completion handler layered onto async
1051
1222
  severity: important
1052
1223
  enforcement: lint
1053
- mechanism: 'custom regex: DispatchQueue|DispatchSemaphore|DispatchGroup in async-adopted paths'
1224
+ mechanism: "custom regex: DispatchQueue|DispatchSemaphore|DispatchGroup in async-adopted paths"
1054
1225
  rationale: readability
1055
1226
 
1056
1227
  - id: CONC-05
@@ -1115,6 +1286,10 @@ rules:
1115
1286
  - id: TEST-06
1116
1287
  title: Tests mirror the source tree across the module's declared layers
1117
1288
  severity: important
1289
+ predicate: mirror_required
1290
+ params:
1291
+ test_role: test.root
1292
+ source_role: source.root
1118
1293
  enforcement: scan
1119
1294
  rationale: testability
1120
1295
  metric: "screens with production logic and no mirrored test folder (target 0)"
@@ -1184,7 +1359,7 @@ rules:
1184
1359
  title: Feature flags are resolved at the boundary and carry a removal condition
1185
1360
  severity: important
1186
1361
  enforcement: lint
1187
- mechanism: 'custom regex: flag access inside a view body'
1362
+ mechanism: "custom regex: flag access inside a view body"
1188
1363
  rationale: flexibility
1189
1364
 
1190
1365
  - id: FLEX-05
@@ -1353,7 +1528,7 @@ rules:
1353
1528
  title: Our own deprecations name a replacement and have a removal condition
1354
1529
  severity: important
1355
1530
  enforcement: lint
1356
- mechanism: 'custom regex: @available(*, deprecated) without a message:, and a deprecated declaration still referenced in-module'
1531
+ mechanism: "custom regex: @available(*, deprecated) without a message:, and a deprecated declaration still referenced in-module"
1357
1532
  rationale: flexibility
1358
1533
  check: >
1359
1534
  A deprecation without a stated replacement is a complaint, not a migration. Every
@@ -1376,7 +1551,7 @@ rules:
1376
1551
  title: Availability gates below the deployment target are removed
1377
1552
  severity: suggestion
1378
1553
  enforcement: lint
1379
- mechanism: 'custom regex: @available / #available naming a version at or below the package deployment target'
1554
+ mechanism: "custom regex: @available / #available naming a version at or below the package deployment target"
1380
1555
  rationale: readability
1381
1556
  check: >
1382
1557
  An `if #available(iOS 16)` in a module that already requires iOS 17 is dead branching a
@@ -1402,7 +1577,7 @@ rules:
1402
1577
  title: A TODO carries an owner or a ticket, and no FIXME survives a release
1403
1578
  severity: suggestion
1404
1579
  enforcement: lint
1405
- mechanism: 'custom regex: TODO or FIXME without a bracketed tag or ticket reference'
1580
+ mechanism: "custom regex: TODO or FIXME without a bracketed tag or ticket reference"
1406
1581
  rationale: readability
1407
1582
  metric: "untagged TODO count (target 0), FIXME count (target 0)"
1408
1583
  check: >
@@ -1412,7 +1587,19 @@ rules:
1412
1587
 
1413
1588
  # ── A11Y ──────────────────────────────────────────────────────────────────
1414
1589
  - id: A11Y-01
1415
- scope: { frameworks: [swiftui], paths: ["**/*View.swift", "**/*Screen.swift", "**/*Scene.swift", "**/*Cell.swift", "**/*Configuration.swift", "**/*+Modifiers.swift"] }
1590
+ scope:
1591
+ {
1592
+ frameworks: [swiftui],
1593
+ paths:
1594
+ [
1595
+ "**/*View.swift",
1596
+ "**/*Screen.swift",
1597
+ "**/*Scene.swift",
1598
+ "**/*Cell.swift",
1599
+ "**/*Configuration.swift",
1600
+ "**/*+Modifiers.swift",
1601
+ ],
1602
+ }
1416
1603
  scope_reason: SwiftUI accessibility modifiers
1417
1604
  title: An identifier from the shared source on every interactive element
1418
1605
  severity: important
@@ -1421,7 +1608,19 @@ rules:
1421
1608
  rationale: testability
1422
1609
 
1423
1610
  - id: A11Y-02
1424
- scope: { frameworks: [swiftui], paths: ["**/*View.swift", "**/*Screen.swift", "**/*Scene.swift", "**/*Cell.swift", "**/*Configuration.swift", "**/*+Modifiers.swift"] }
1611
+ scope:
1612
+ {
1613
+ frameworks: [swiftui],
1614
+ paths:
1615
+ [
1616
+ "**/*View.swift",
1617
+ "**/*Screen.swift",
1618
+ "**/*Scene.swift",
1619
+ "**/*Cell.swift",
1620
+ "**/*Configuration.swift",
1621
+ "**/*+Modifiers.swift",
1622
+ ],
1623
+ }
1425
1624
  scope_reason: SwiftUI accessibility modifiers
1426
1625
  title: Localized VoiceOver label, plus a hint where the action is not obvious
1427
1626
  severity: important
@@ -1429,7 +1628,19 @@ rules:
1429
1628
  rationale: accessibility
1430
1629
 
1431
1630
  - id: A11Y-03
1432
- scope: { frameworks: [swiftui], paths: ["**/*View.swift", "**/*Screen.swift", "**/*Scene.swift", "**/*Cell.swift", "**/*Configuration.swift", "**/*+Modifiers.swift"] }
1631
+ scope:
1632
+ {
1633
+ frameworks: [swiftui],
1634
+ paths:
1635
+ [
1636
+ "**/*View.swift",
1637
+ "**/*Screen.swift",
1638
+ "**/*Scene.swift",
1639
+ "**/*Cell.swift",
1640
+ "**/*Configuration.swift",
1641
+ "**/*+Modifiers.swift",
1642
+ ],
1643
+ }
1433
1644
  scope_reason: SwiftUI accessibility modifiers
1434
1645
  title: Minimum 44x44 tap target; grouped content exposes one meaningful element
1435
1646
  severity: important
@@ -1437,7 +1648,19 @@ rules:
1437
1648
  rationale: accessibility
1438
1649
 
1439
1650
  - id: A11Y-04
1440
- scope: { frameworks: [swiftui], paths: ["**/*View.swift", "**/*Screen.swift", "**/*Scene.swift", "**/*Cell.swift", "**/*Configuration.swift", "**/*+Modifiers.swift"] }
1651
+ scope:
1652
+ {
1653
+ frameworks: [swiftui],
1654
+ paths:
1655
+ [
1656
+ "**/*View.swift",
1657
+ "**/*Screen.swift",
1658
+ "**/*Scene.swift",
1659
+ "**/*Cell.swift",
1660
+ "**/*Configuration.swift",
1661
+ "**/*+Modifiers.swift",
1662
+ ],
1663
+ }
1441
1664
  scope_reason: SwiftUI accessibility modifiers
1442
1665
  title: Dynamic Type does not break layout at the largest accessibility sizes
1443
1666
  severity: important
@@ -1446,7 +1669,19 @@ rules:
1446
1669
  check: No fixed-height container holding scalable text.
1447
1670
 
1448
1671
  - id: A11Y-05
1449
- scope: { frameworks: [swiftui], paths: ["**/*View.swift", "**/*Screen.swift", "**/*Scene.swift", "**/*Cell.swift", "**/*Configuration.swift", "**/*+Modifiers.swift"] }
1672
+ scope:
1673
+ {
1674
+ frameworks: [swiftui],
1675
+ paths:
1676
+ [
1677
+ "**/*View.swift",
1678
+ "**/*Screen.swift",
1679
+ "**/*Scene.swift",
1680
+ "**/*Cell.swift",
1681
+ "**/*Configuration.swift",
1682
+ "**/*+Modifiers.swift",
1683
+ ],
1684
+ }
1450
1685
  scope_reason: SwiftUI accessibility modifiers
1451
1686
  title: RTL mirrors correctly; no leading/trailing hardcoded as left/right
1452
1687
  severity: important
@@ -1465,7 +1700,7 @@ rules:
1465
1700
  title: Lists and grids use lazy containers with stable identity
1466
1701
  severity: important
1467
1702
  enforcement: lint
1468
- mechanism: 'custom regex: ForEach over indices as identity; VStack over a large collection'
1703
+ mechanism: "custom regex: ForEach over indices as identity; VStack over a large collection"
1469
1704
  rationale: performance
1470
1705
 
1471
1706
  - id: PERF-03
@@ -1478,5 +1713,105 @@ rules:
1478
1713
  title: No per-render construction of formatters, calendars or regexes
1479
1714
  severity: suggestion
1480
1715
  enforcement: lint
1481
- mechanism: 'custom regex: DateFormatter(|NumberFormatter(|Regex( inside a view body'
1716
+ mechanism: "custom regex: DateFormatter(|NumberFormatter(|Regex( inside a view body"
1482
1717
  rationale: performance
1718
+
1719
+ # --- Tree, mechanically checked (added 0.2.0) ----------------------------
1720
+ - id: STRUCT-09
1721
+ title: A service operation's request and response live together, and neither travels alone
1722
+ severity: important
1723
+ enforcement: lint
1724
+ predicate: pair_required_in_dir
1725
+ params:
1726
+ slot: ServiceModelDir
1727
+ container_role: service.dir
1728
+ left_role: service.request
1729
+ right_role: service.response
1730
+ pairing_slot: ServiceModelPairing
1731
+ right_only_value: response-only
1732
+ rationale: navigability
1733
+ check: >
1734
+ A directory holding one half of a service operation holds the other. A lone response means
1735
+ the request is inlined somewhere a reader will not find it. Under `response-only` a lone
1736
+ response is normal and only a lone request is the finding.
1737
+
1738
+ - id: STRUCT-10
1739
+ title: A screen ships the construction seam its module's assembly shape declares
1740
+ severity: important
1741
+ enforcement: lint
1742
+ predicate: file_required_in_dir
1743
+ params:
1744
+ slot: ScreenAssemblyShape
1745
+ slot_value: per-screen-factory
1746
+ role: screen.factory
1747
+ applies_when: the module binds ScreenAssemblyShape to per-screen-factory. Otherwise DISABLED.
1748
+ rationale: module boundaries
1749
+ check: Under per-screen-factory each screen ships its own seam; under shared-factory the seam is one per module.
1750
+
1751
+ - id: STRUCT-11
1752
+ title: UI state lives where the module decided it lives
1753
+ severity: important
1754
+ enforcement: lint
1755
+ predicate: file_required_in_dir
1756
+ params:
1757
+ slot: UIStateHolder
1758
+ slot_value: separate-state-type
1759
+ role: screen.state
1760
+ trigger_role: screen.viewmodel
1761
+ trigger_pattern: '(FormField|\.Section\b|var\s+(is|selected|shows|expanded)\w*(Presented|Expanded|Sheet|Index|Visible|Selected|Shown)\b)'
1762
+ rationale: testability
1763
+ check: >
1764
+ A screen with bound form objects or visual state, in a module that separated that state out,
1765
+ carries the state type. A screen with neither is not missing anything.
1766
+
1767
+ - id: STRUCT-12
1768
+ title: The value type an extracted view renders holds values, not behaviour
1769
+ severity: blocking
1770
+ enforcement: lint
1771
+ predicate: forbidden_pattern
1772
+ params:
1773
+ slot: SubviewShape
1774
+ slot_value: folder-per-subview
1775
+ subject_role: subview.configuration
1776
+ pattern: '^\s+(let|var)\s+\w+\s*:\s*(@escaping\s+)?\('
1777
+ rationale: testability
1778
+ check: >
1779
+ A closure stored in the value type makes it non-comparable and drags the caller's lifetime
1780
+ into it. Actions reach the view as its own parameters.
1781
+
1782
+ - id: STRUCT-13
1783
+ title: An extracted view takes values, never the screen's view model
1784
+ severity: blocking
1785
+ enforcement: lint
1786
+ predicate: forbidden_pattern
1787
+ params:
1788
+ slot: SubviewShape
1789
+ subject_role: subview.view
1790
+ pattern: '^\s+(let|var)\s+\w+\s*:\s*(any\s+)?\w*ViewModel\b'
1791
+ rationale: testability
1792
+ check: >
1793
+ A view holding the view model can reach anything, so nothing about it can be asserted from
1794
+ its inputs, and it cannot be previewed without constructing the whole screen.
1795
+
1796
+ - id: STRUCT-14
1797
+ title: Fixture data lives with the fixtures, not inside the thing that serves it
1798
+ severity: suggestion
1799
+ enforcement: judgement
1800
+ predicate: none
1801
+ params: {}
1802
+ rationale: readability
1803
+ check: >
1804
+ A scripted implementation that also carries its payload literals mixes which scenario to
1805
+ answer with the content of the answer. The tool surfaces the size ratio; the split is a reading.
1806
+
1807
+ - id: NAME-07
1808
+ title: A positional index carries the same label everywhere
1809
+ severity: suggestion
1810
+ enforcement: lint
1811
+ predicate: naming_pattern
1812
+ params:
1813
+ glob: '**/*.swift'
1814
+ declaration: '\((for|_)\s+(?:index|\w+Index)\s*:\s*Int'
1815
+ accept_from_vocabulary: IndexLabel
1816
+ rationale: readability
1817
+ check: One spelling for "at this position" across the module; two make the call sites read as two concepts.
@@ -109,7 +109,6 @@ missing_docs:
109
109
  excludes_trivial_init: true
110
110
 
111
111
  custom_rules:
112
-
113
112
  # ── SEC ─────────────────────────────────────────────────────────────────
114
113
  sec_01_no_plaintext_persistence:
115
114
  name: "SEC-01 sensitive value in unprotected storage"
@@ -301,7 +300,7 @@ custom_rules:
301
300
  # ── READ / SAFE ─────────────────────────────────────────────────────────
302
301
  read_02_ai_attribution:
303
302
  name: "READ-02 AI or tool attribution"
304
- regex: '(?i)(generated by (claude|copilot|ai)|🤖 generated|co-authored-by:.*(claude|anthropic|copilot))'
303
+ regex: "(?i)(generated by (claude|copilot|ai)|🤖 generated|co-authored-by:.*(claude|anthropic|copilot))"
305
304
  message: "No AI or tool attribution in source, commits or PRs."
306
305
  severity: error
307
306