@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.
- package/CHANGELOG.md +76 -6
- package/README.md +15 -8
- package/README.tr.md +15 -8
- package/docs/FIGMA_PIPELINE.md +3 -3
- package/docs/adr/0006-skills-core-external-split.md +1 -1
- package/docs/adr/0009-claude-stack-skills-plugin-only.md +31 -0
- package/docs/adr/README.md +1 -0
- package/docs/architecture.md +7 -7
- package/docs/ecosystem.md +28 -28
- package/docs/features.md +5 -5
- package/index.js +2 -0
- package/install/_codex-agents.mjs +11 -2
- package/install/_common.mjs +65 -1
- package/install/_dev-only-files.mjs +0 -1
- package/install/_platform-filter.mjs +73 -7
- package/install/_plugin-skills.mjs +19 -8
- package/install/claude.mjs +144 -59
- package/install/codex.mjs +28 -3
- package/install/copilot.mjs +36 -11
- package/install/index.mjs +6 -2
- package/install/templates/codex-instructions.md +1 -1
- package/install/templates/copilot-instructions.md +3 -3
- package/package.json +1 -2
- package/pipeline/commands/multi-agent/SKILL.md +2 -0
- package/pipeline/commands/multi-agent/analysis/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/build-optimize/SKILL.md +9 -9
- package/pipeline/commands/multi-agent/channels/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +186 -0
- package/pipeline/commands/multi-agent/dev/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/help/SKILL.md +19 -4
- package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/jira/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/prune-prompts/SKILL.md +81 -0
- package/pipeline/commands/multi-agent/resume/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/{ship → resume-local}/SKILL.md +8 -8
- package/pipeline/commands/multi-agent/setup/SKILL.md +5 -5
- package/pipeline/commands/multi-agent/stack/SKILL.md +55 -43
- package/pipeline/commands/multi-agent/store-ready/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/sync/SKILL.md +18 -11
- package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/uninstall/SKILL.md +2 -0
- package/pipeline/commands/multi-agent/update/SKILL.md +1 -1
- package/pipeline/lib/issue-fetcher.sh +1 -1
- package/pipeline/lib/parse-complaints.sh +306 -0
- package/pipeline/multi-agent-refs/channels/wiki.md +3 -3
- package/pipeline/multi-agent-refs/complaint-analysis-template.md +99 -0
- package/pipeline/multi-agent-refs/component-dispatch.md +6 -6
- package/pipeline/multi-agent-refs/cross-cli-contract.md +16 -16
- package/pipeline/multi-agent-refs/features/external-context-injection.md +1 -1
- package/pipeline/multi-agent-refs/features/stack-skill-routing.md +5 -5
- package/pipeline/multi-agent-refs/generate-issue.md +1 -1
- package/pipeline/multi-agent-refs/phases/modes.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +7 -7
- package/pipeline/multi-agent-refs/phases/phase-2-planning.md +5 -5
- package/pipeline/multi-agent-refs/phases/phase-3-dev.md +3 -3
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +12 -12
- package/pipeline/multi-agent-refs/phases/phase-5-test.md +1 -1
- package/pipeline/multi-agent-refs/tracker-contract.md +1 -1
- package/pipeline/multi-agent-refs/wiki-capture.md +2 -2
- package/pipeline/preferences-template.json +13 -5
- package/pipeline/rules/figma-pipeline.md +2 -2
- package/pipeline/schemas/agent-state.schema.json +1 -1
- package/pipeline/schemas/complaint-analysis-spec.schema.json +216 -0
- package/pipeline/schemas/migrations/prefs-2.5.0-to-2.6.0.mjs +46 -0
- package/pipeline/schemas/prefs.schema.json +276 -66
- package/pipeline/schemas/token-budget.json +2 -2
- package/pipeline/scripts/_stack-routing.mjs +79 -0
- package/pipeline/scripts/audit-log-rotate.sh +4 -1
- package/pipeline/scripts/build-skills-index.mjs +11 -0
- package/pipeline/scripts/build-stack-plugins.mjs +28 -60
- package/pipeline/scripts/check-derived-drift.mjs +52 -28
- package/pipeline/scripts/gc-worktrees.sh +4 -1
- package/pipeline/scripts/gen-skills-index.mjs +1 -1
- package/pipeline/scripts/match-skills.mjs +8 -2
- package/pipeline/scripts/migrate-prefs.mjs +28 -20
- package/pipeline/scripts/phase-tracker.sh +13 -5
- package/pipeline/scripts/phase0-exit-gate.mjs +3 -2
- package/pipeline/scripts/run-aggregator.mjs +7 -2
- package/pipeline/scripts/scan-agent-config.sh +1 -1
- package/pipeline/scripts/skill-conformance.mjs +165 -30
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +1 -1
- package/pipeline/scripts/test-gap-rules/android.json +25 -0
- package/pipeline/scripts/test-gap-rules/ios.json +34 -0
- package/pipeline/scripts/test-gap-rules/node.json +29 -0
- package/pipeline/scripts/test-gap-rules/python.json +25 -0
- package/pipeline/scripts/uninstall.mjs +158 -11
- package/pipeline/scripts/validate-complaint-doc.mjs +229 -0
- package/pipeline/scripts/validate-reviewer.mjs +9 -3
- package/pipeline/skills/.skill-manifest.json +156 -108
- package/pipeline/skills/.skills-index.json +449 -12
- package/pipeline/skills/shared/README.md +14 -10
- package/pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-build-optimize/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-complaint-analysis/SKILL.md +49 -0
- package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-prune-prompts/SKILL.md +83 -0
- package/pipeline/skills/shared/core/{multi-agent-ship → multi-agent-resume-local}/SKILL.md +6 -6
- package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +79 -22
- package/pipeline/skills/shared/core/multi-agent-store-ready/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +8 -8
- package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +1 -1
- package/pipeline/skills/shared/external/ios-coding-standard/modules/_TEMPLATE.yml +2 -2
- package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +368 -33
- package/pipeline/skills/shared/external/ios-coding-standard/references/swiftlint.draft.yml +1 -2
- package/pipeline/skills/shared/external/ios-coding-standard/scripts/check_structure.py +765 -0
- package/pipeline/skills/shared/external/ios-module-structure/SKILL.md +75 -0
- package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +131 -0
- package/pipeline/skills/shared/external/ios-module-structure/references/rules.yml +559 -0
- package/pipeline/skills/shared/external/ios-module-structure/scripts/check_structure.py +765 -0
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +53 -10
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +4 -3
- 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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
971
|
-
mechanism:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|