@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.
- package/CHANGELOG.md +186 -6
- package/README.md +19 -12
- package/README.tr.md +19 -12
- package/SECURITY.md +43 -0
- package/docs/FIGMA_PIPELINE.md +3 -3
- package/docs/adr/0006-skills-core-external-split.md +1 -1
- package/docs/adr/0007-multi-tool-adapter-framework.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 +13 -13
- package/docs/ecosystem.md +31 -31
- package/docs/features.md +5 -5
- package/index.js +6 -1
- package/install/_codex-agents.mjs +11 -2
- package/install/_common.mjs +109 -3
- package/install/_dev-only-files.mjs +0 -1
- package/install/_platform-filter.mjs +54 -113
- package/install/_plugin-skills.mjs +36 -36
- package/install/claude.mjs +251 -61
- package/install/codex.mjs +28 -6
- package/install/copilot.mjs +69 -9
- package/install/index.mjs +9 -3
- package/install/templates/codex-instructions.md +1 -1
- package/install/templates/copilot-instructions.md +3 -3
- package/package.json +2 -3
- 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/refactor/SKILL.md +36 -1
- 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/scan/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/setup/SKILL.md +5 -5
- package/pipeline/commands/multi-agent/stack/SKILL.md +62 -40
- 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 +4 -4
- package/pipeline/lib/issue-fetcher.sh +1 -1
- package/pipeline/lib/parse-complaints.sh +316 -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/operations.md +7 -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/phases/phase-7-report.md +6 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +3 -2
- package/pipeline/multi-agent-refs/wiki-capture.md +2 -2
- package/pipeline/preferences-template.json +18 -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 +296 -66
- package/pipeline/schemas/token-budget.json +2 -2
- package/pipeline/scripts/README.md +4 -3
- 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 +55 -28
- package/pipeline/scripts/gc-worktrees.sh +4 -1
- package/pipeline/scripts/gen-skills-index.mjs +1 -1
- package/pipeline/scripts/match-skills.mjs +12 -2
- package/pipeline/scripts/migrate-prefs.mjs +33 -21
- package/pipeline/scripts/phase-tracker.sh +32 -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 +160 -11
- package/pipeline/scripts/usage-report.mjs +426 -0
- package/pipeline/scripts/validate-complaint-doc.mjs +250 -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-refactor/SKILL.md +153 -90
- 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 +89 -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/core/multi-agent-update/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
|
@@ -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.
|