@mmerterden/multi-agent-pipeline 13.5.0 → 14.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 +243 -0
- package/README.md +3 -3
- package/docs/features.md +1 -1
- package/install/_common.mjs +73 -0
- package/install/_mcp-register.mjs +70 -31
- package/install/_plugin-skills.mjs +73 -14
- package/install/claude.mjs +28 -4
- package/install/codex.mjs +33 -2
- package/install/copilot.mjs +145 -9
- package/install/index.mjs +10 -6
- package/install/templates/copilot-instructions.md +1 -1
- package/package.json +1 -1
- package/pipeline/agents/code-reviewer.md +58 -1
- package/pipeline/commands/multi-agent/SKILL.md +7 -5
- package/pipeline/commands/multi-agent/analysis/SKILL.md +7 -7
- package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/build-optimize/SKILL.md +7 -7
- package/pipeline/commands/multi-agent/channels/SKILL.md +5 -5
- package/pipeline/commands/multi-agent/dev/SKILL.md +23 -18
- package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +19 -13
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +14 -12
- package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +17 -12
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/help/SKILL.md +4 -4
- package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +4 -4
- package/pipeline/commands/multi-agent/resume/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/review/SKILL.md +5 -5
- package/pipeline/commands/multi-agent/scan/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/search/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/setup/SKILL.md +6 -6
- package/pipeline/commands/multi-agent/{finish → ship}/SKILL.md +12 -12
- package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/update/SKILL.md +5 -2
- package/pipeline/commands/sim-test.md +2 -2
- package/pipeline/lib/credential-store-resolver.sh +16 -0
- package/pipeline/lib/credential-store.sh +47 -4
- package/pipeline/lib/fetch-figma-annotations.sh +26 -28
- package/pipeline/lib/figma-screenshot.sh +28 -39
- package/pipeline/lib/figma-token.sh +63 -0
- package/pipeline/multi-agent-refs/analysis-template.md +1 -1
- package/pipeline/multi-agent-refs/android-guide.md +1 -1
- package/pipeline/multi-agent-refs/channels/issue-comment.md +1 -1
- package/pipeline/multi-agent-refs/component-dispatch.md +2 -2
- package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -4
- package/pipeline/multi-agent-refs/features/dev-critic.md +2 -2
- package/pipeline/multi-agent-refs/features/model-fallback.md +35 -2
- package/pipeline/multi-agent-refs/features/plan-todos.md +1 -1
- package/pipeline/multi-agent-refs/features/repo-map.md +1 -1
- package/pipeline/multi-agent-refs/features/review-multi-repo.md +3 -3
- package/pipeline/multi-agent-refs/features/shadow-git.md +1 -1
- package/pipeline/multi-agent-refs/features/skill-conformance.md +116 -0
- package/pipeline/multi-agent-refs/features/verify-by-test.md +1 -1
- package/pipeline/multi-agent-refs/generate-issue.md +1 -1
- package/pipeline/multi-agent-refs/multi-repo-integration-build.md +1 -1
- package/pipeline/multi-agent-refs/phases/log-format.md +4 -4
- package/pipeline/multi-agent-refs/phases/modes.md +7 -7
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +13 -11
- package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +17 -15
- package/pipeline/multi-agent-refs/phases/phase-2-planning.md +7 -7
- package/pipeline/multi-agent-refs/phases/phase-3-dev.md +28 -13
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +90 -58
- package/pipeline/multi-agent-refs/phases/phase-5-test.md +7 -7
- package/pipeline/multi-agent-refs/phases/phase-6-commit.md +8 -8
- package/pipeline/multi-agent-refs/phases/phase-7-report.md +8 -8
- package/pipeline/multi-agent-refs/phases.md +13 -13
- package/pipeline/multi-agent-refs/progress-contract.md +2 -2
- package/pipeline/multi-agent-refs/rules.md +7 -5
- package/pipeline/multi-agent-refs/swiftui-guide.md +1 -1
- package/pipeline/multi-agent-refs/tracker-contract.md +16 -15
- package/pipeline/preferences-template.json +7 -1
- package/pipeline/rules/figma-pipeline.md +2 -2
- package/pipeline/schemas/agent-state.schema.json +333 -79
- package/pipeline/schemas/criteria-manifest.schema.json +228 -0
- package/pipeline/schemas/migrations/prefs-2.4.0-to-2.5.0.mjs +64 -0
- package/pipeline/schemas/prefs.schema.json +118 -262
- package/pipeline/schemas/reviewer-output.schema.json +48 -3
- package/pipeline/schemas/token-budget.json +34 -10
- package/pipeline/schemas/triage-output.schema.json +112 -27
- package/pipeline/scripts/cost-table.json +7 -4
- package/pipeline/scripts/gc-worktrees.sh +1 -1
- package/pipeline/scripts/gen-mode-dispatch.mjs +6 -6
- package/pipeline/scripts/match-skills.mjs +37 -4
- package/pipeline/scripts/migrate-prefs.mjs +88 -17
- package/pipeline/scripts/phase-tracker.sh +14 -3
- package/pipeline/scripts/pre-commit-check.sh +49 -2
- package/pipeline/scripts/skill-conformance.mjs +960 -0
- package/pipeline/scripts/smoke-schema-validation.sh +17 -4
- package/pipeline/scripts/uninstall.mjs +35 -9
- package/pipeline/scripts/validate-reviewer.mjs +108 -1
- package/pipeline/skills/.skill-manifest.json +1 -1
- package/pipeline/skills/.skills-index.json +36 -9
- package/pipeline/skills/shared/README.md +15 -12
- package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +1 -0
- package/pipeline/skills/shared/core/apple-archive-compliance/references/rules.yml +167 -0
- package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +1 -0
- package/pipeline/skills/shared/core/google-play-compliance/references/rules.yml +184 -0
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +10 -10
- package/pipeline/skills/shared/core/multi-agent-analysis/SKILL.md +4 -4
- package/pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md +3 -3
- package/pipeline/skills/shared/core/multi-agent-build-optimize/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-create-jira/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +6 -5
- package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +7 -6
- package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +4 -3
- package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +4 -4
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +5 -5
- package/pipeline/skills/shared/core/multi-agent-scan/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-search/SKILL.md +1 -1
- package/pipeline/skills/shared/core/{multi-agent-finish → multi-agent-ship}/SKILL.md +8 -8
- package/pipeline/skills/shared/external/ios-coding-standard/SKILL.md +44 -5
- package/pipeline/skills/shared/external/ios-coding-standard/modules/_TEMPLATE.yml +82 -0
- package/pipeline/skills/shared/external/ios-coding-standard/references/STANDARD.md +169 -10
- package/pipeline/skills/shared/external/ios-coding-standard/references/lint-local.sh +13 -1
- package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +335 -16
- package/pipeline/skills/skills-index.md +11 -8
|
@@ -20,10 +20,10 @@ Runs the full 8-phase pipeline **without creating a worktree** + **skipping all
|
|
|
20
20
|
| `multi-agent-autopilot "task"` | Full 8 phases | ✅ | ❌ |
|
|
21
21
|
| `multi-agent-local "task"` | Full 8 phases | ❌ | ✅ |
|
|
22
22
|
| **`multi-agent-local-autopilot "task"`** | **Full 8 phases** | **❌** | **❌** |
|
|
23
|
-
| `multi-agent-dev "task"` | Init → Dev → Commit → Report | ✅ | ✅ |
|
|
24
|
-
| `multi-agent-dev-autopilot "task"` | Init → Dev → Commit → Report | ✅ | ❌ |
|
|
25
|
-
| `multi-agent-dev-local "task"` | Init → Dev → Commit → Report | ❌ | ✅ |
|
|
26
|
-
| `multi-agent-dev-local-autopilot "task"` | Init → Dev → Commit → Report | ❌ | ❌ (fastest) |
|
|
23
|
+
| `multi-agent-dev "task"` | Init → Dev → Review → Test → Commit → Report | ✅ | ✅ |
|
|
24
|
+
| `multi-agent-dev-autopilot "task"` | Init → Dev → Review → Commit → Report | ✅ | ❌ |
|
|
25
|
+
| `multi-agent-dev-local "task"` | Init → Dev → Review → Commit → Report | ❌ | ✅ |
|
|
26
|
+
| `multi-agent-dev-local-autopilot "task"` | Init → Dev → Review → Commit → Report | ❌ | ❌ (fastest) |
|
|
27
27
|
|
|
28
28
|
## What changes
|
|
29
29
|
|
|
@@ -35,19 +35,19 @@ Skip Phase 0-3 and review a diff only. Input shapes: a PR (`#N`, `repo#N`, GitHu
|
|
|
35
35
|
|
|
36
36
|
**Claude Code (2 in parallel):**
|
|
37
37
|
- Agent 1: `claude-fable-5` → security + architecture
|
|
38
|
-
- Agent 2: `claude-sonnet-
|
|
38
|
+
- Agent 2: `claude-sonnet-5` → general quality
|
|
39
39
|
|
|
40
40
|
**Copilot CLI (3 in parallel):**
|
|
41
|
-
- Agent 1: `claude-opus-
|
|
41
|
+
- Agent 1: `claude-opus-5` → security + architecture (Fable 5 is not offered on Copilot CLI)
|
|
42
42
|
- Agent 2: `gpt-5.4` → edge cases, different perspective
|
|
43
|
-
- Agent 3: `claude-sonnet-
|
|
43
|
+
- Agent 3: `claude-sonnet-5` → general quality
|
|
44
44
|
|
|
45
45
|
4. **Store-compliance cross-reference** - if iOS/Android release-relevant files changed, the matching catalog is loaded:
|
|
46
46
|
|
|
47
47
|
| Platform | Trigger files | Catalog to load |
|
|
48
48
|
|---|---|---|
|
|
49
|
-
| iOS | `**/Info.plist`, `**/PrivacyInfo.xcprivacy`, `**/*.entitlements`, `**/*App.swift`, `**/AppDelegate*.swift`, `**/SceneDelegate*.swift`, `**/project.pbxproj` |
|
|
50
|
-
| Android | `**/AndroidManifest.xml`, `**/build.gradle(.kts)`, `**/proguard-rules.pro`, `**/network_security_config.xml` |
|
|
49
|
+
| iOS | `**/Info.plist`, `**/PrivacyInfo.xcprivacy`, `**/*.entitlements`, `**/*App.swift`, `**/AppDelegate*.swift`, `**/SceneDelegate*.swift`, `**/project.pbxproj` | `$HOME/.claude/skills/apple-archive-compliance/SKILL.md` - 18 rules + ITMS refs |
|
|
50
|
+
| Android | `**/AndroidManifest.xml`, `**/build.gradle(.kts)`, `**/proguard-rules.pro`, `**/network_security_config.xml` | `$HOME/.claude/skills/google-play-compliance/SKILL.md` - 21 rules + Play policy refs |
|
|
51
51
|
|
|
52
52
|
It appends the catalog's `ruleID` + the Apple ITMS / Play policy ref to each finding:
|
|
53
53
|
- iOS example: `(apple-archive-compliance / info-plist - Guideline 5.1.1)`
|
|
@@ -42,7 +42,7 @@ JSON: `{ query, matched_tasks, tasks: [{ task_id, project, branch, date, matches
|
|
|
42
42
|
|
|
43
43
|
1. Script:
|
|
44
44
|
```bash
|
|
45
|
-
bash "$
|
|
45
|
+
bash "$HOME/.claude/scripts/search-logs.sh" "$ARGUMENTS"
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
2. Exit code:
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: multi-agent-
|
|
2
|
+
name: multi-agent-ship
|
|
3
3
|
language: en
|
|
4
4
|
description: "Continue already-done LOCAL work through the pipeline tail: Review → Build+Test → Commit/PR → Report (technical analysis + Jira test-scenario comment). No dev phase. Use when local work is already done and only review, build, commit and reporting remain."
|
|
5
5
|
user-invocable: true
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
# multi-agent
|
|
8
|
+
# multi-agent ship - Take Existing Branch Work Through the Pipeline Tail
|
|
9
9
|
|
|
10
|
-
You already wrote (and maybe hand-tested) the change on the current branch
|
|
10
|
+
You already wrote (and maybe hand-tested) the change on the current branch, or committed it outside the pipeline entirely. `ship` picks up from there and runs the **pipeline tail** over that existing work in one command, without re-developing. (As of v14.0.0 the `--dev` family reviews its own output, so this is for work with no pipeline run behind it.)
|
|
11
11
|
|
|
12
12
|
## Pipeline
|
|
13
13
|
|
|
@@ -34,14 +34,14 @@ Phases 1-3 (Analysis / Planning / Dev) are skipped by design - the branch's lo
|
|
|
34
34
|
## Input
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
multi-agent
|
|
38
|
-
multi-agent
|
|
39
|
-
multi-agent
|
|
40
|
-
multi-agent
|
|
37
|
+
multi-agent ship # current branch vs base; Jira id from branch name
|
|
38
|
+
multi-agent ship PROJ-12345 # explicit Jira id for the Phase 7 comment
|
|
39
|
+
multi-agent ship --base develop # override base branch for the diff
|
|
40
|
+
multi-agent ship autopilot # no gate prompts: auto-fix, auto-PR, auto-comment
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
## Notes
|
|
44
44
|
|
|
45
45
|
- Build+Test is the automated success gate (not the interactive device user-test - that is `multi-agent:manual-test`). If the repo has no tests, it reports "no tests present" - never fabricates results.
|
|
46
46
|
- Commit/PR follows house rules: conventional message, `Ref: #N` (never Closes/Fixes), NO AI/bot attribution. PR opened only if one does not already exist.
|
|
47
|
-
- Full phase contract lives in the Claude Code command `commands/multi-agent/
|
|
47
|
+
- Full phase contract lives in the Claude Code command `commands/multi-agent/ship/SKILL.md`; this skill is the Copilot-CLI counterpart.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ios-coding-standard
|
|
3
|
-
description: "The iOS coding-standard rule registry:
|
|
3
|
+
description: "The iOS coding-standard rule registry: 99 stable-ID rules across readability, security, service layer, business rules, concurrency, testing, module boundaries, naming and visibility, each with a severity and enforcement kind. Use when writing or reviewing Swift and you need the project rule rather than an opinion, or on a persistence, logging or business-rule-placement question."
|
|
4
4
|
user-invocable: true
|
|
5
|
+
standards-registry: references/rules.yml
|
|
5
6
|
---
|
|
6
7
|
|
|
7
8
|
# iOS coding standard
|
|
@@ -12,10 +13,20 @@ is not.
|
|
|
12
13
|
|
|
13
14
|
| File | What it is | When to read it |
|
|
14
15
|
|---|---|---|
|
|
15
|
-
| `references/rules.yml` |
|
|
16
|
+
| `references/rules.yml` | 99 rules with stable IDs, severity, enforcement kind and a `check` describing what counts as a violation | before asserting that something is or is not a violation |
|
|
16
17
|
| `references/STANDARD.md` | the same rules taught with before/after Swift, for a human | when you need the reasoning or an example, not just the rule |
|
|
17
18
|
| `references/swiftlint.draft.yml` | the mechanically-enforceable subset as a SwiftLint config | when wiring lint into a project |
|
|
18
19
|
| `references/lint-local.sh` | runs that config over one module, with a baseline mode | when grandfathering existing violations so only new ones surface |
|
|
20
|
+
| `modules/<Module>.yml` | per-module overlay: vocabulary bindings, dialect choices, zero-instance prohibitions, carve-outs, known findings. **Project-local, never shipped** - see `modules/_TEMPLATE.yml` | before auditing a module, and before raising any slot-bound rule |
|
|
21
|
+
| `references/EXAMPLES.md` | optional, project-local: a worked ✗/✓ pair per judgement rule, keyed by ID, drawn from real code in YOUR repo | when citing a judgement rule, so the fix and the audit read the same picture |
|
|
22
|
+
|
|
23
|
+
**Two files are deliberately absent from the shipped skill.** `modules/<Module>.yml` and
|
|
24
|
+
`references/EXAMPLES.md` quote a specific codebase: module paths, symbol names, real call-site
|
|
25
|
+
counts, before/after snippets. That is exactly what makes them useful and exactly why they cannot
|
|
26
|
+
travel - a shipped overlay would bind slots to another project's dialect, and a shipped example
|
|
27
|
+
would teach its naming. Write them in your own installation; `modules/_TEMPLATE.yml` carries the
|
|
28
|
+
shape and the slot list. An absent overlay is not a neutral default: an unbound slot DISABLES its
|
|
29
|
+
rules, and the audit reports that rather than guessing a dialect.
|
|
19
30
|
|
|
20
31
|
## How to use it
|
|
21
32
|
|
|
@@ -33,7 +44,7 @@ is not.
|
|
|
33
44
|
`// standard:exception(<RULE-ID>) <reason> <expiry:YYYY-MM-DD>`. An unmarked
|
|
34
45
|
deviation is a finding; a marked one is a decision.
|
|
35
46
|
|
|
36
|
-
##
|
|
47
|
+
## Three decisions the registry answers, and code usually gets wrong
|
|
37
48
|
|
|
38
49
|
**Persistence.** Read `references/rules.yml → persistence_decision` before reaching for
|
|
39
50
|
storage. The ladder starts at "does this value need to outlive the current flow?"
|
|
@@ -48,10 +59,20 @@ clamp or threshold, choosing a screen state, and policy defaults are **not** - e
|
|
|
48
59
|
belongs to the view model, or to a named domain rule when several screens share it.
|
|
49
60
|
Carry the wire value with its unit in the name and convert where it is read.
|
|
50
61
|
|
|
62
|
+
**Where a business rule lives.** Read `references/rules.yml → RULE-01` before writing one. Three
|
|
63
|
+
homes, cheapest rung first: the **view model** when one screen asks it - stop there, a namespace with
|
|
64
|
+
one consumer is over-hoisting; a **computed property on the entity** when several screens ask it of
|
|
65
|
+
data the entity already carries; a **named rule namespace** when several screens ask it but the
|
|
66
|
+
answer is screen policy rather than entity state. `SVC-08` bans every other layer. The part usually
|
|
67
|
+
skipped is the citation: a rule names the requirement it enforces and its test repeats that token, so
|
|
68
|
+
one grep reaches requirement, code and test - and that citation is what makes a `judgement` rule
|
|
69
|
+
checkable instead of an opinion.
|
|
70
|
+
|
|
51
71
|
## Rule families
|
|
52
72
|
|
|
53
73
|
`READ` readability and section structure · `SEC` secrets, logging, storage ·
|
|
54
|
-
`SVC` service layer and mapping · `
|
|
74
|
+
`SVC` service layer and mapping · `RULE` business-rule surface and traceability ·
|
|
75
|
+
`TEST` testability seams · `MOD` module
|
|
55
76
|
boundaries and imports · `NAME` naming · `FLEX` flexibility and extension points ·
|
|
56
77
|
`CONC` concurrency · `VIS` visibility and access level · `UI` view construction ·
|
|
57
78
|
`PERF` performance · `DEPR` deprecation and retirement.
|
|
@@ -61,9 +82,27 @@ boundaries and imports · `NAME` naming · `FLEX` flexibility and extension poin
|
|
|
61
82
|
The registry is project-configurable, not project-specific: rule bodies describe
|
|
62
83
|
shapes (a mapper doing arithmetic, a logger interpolating a token) rather than named
|
|
63
84
|
modules. A project adds its own vocabulary through a `modules/<Module>.yml` overlay
|
|
64
|
-
|
|
85
|
+
in the `modules/` directory - the module's terms, its allowed dependencies, its validation
|
|
65
86
|
rules - which the audit binds on top of the shared registry. Nothing in the shared
|
|
66
87
|
registry names a module, so it applies unchanged to any SwiftUI codebase.
|
|
88
|
+
Copy `modules/_TEMPLATE.yml` to `modules/<YourModule>.yml` and bind the slots from evidence in your
|
|
89
|
+
own code - each binding wants a count, not a preference, because the count is what tells a dialect
|
|
90
|
+
choice apart from a defect.
|
|
91
|
+
|
|
92
|
+
**Language scope is declared, not assumed.** The registry carries a top-level `scope:` block naming
|
|
93
|
+
the languages and paths its rules apply to. This exists because the rule bodies below were written
|
|
94
|
+
against Swift with SwiftUI view construction: applied unread to an Objective-C or UIKit file they
|
|
95
|
+
would manufacture findings, which buries the real ones and teaches the reader to distrust the run.
|
|
96
|
+
A consumer that cannot honour `scope` must load the registry as reference only and say so.
|
|
97
|
+
|
|
98
|
+
**Some rules govern a choice, not a defect.** Where two shapes are each internally coherent and the
|
|
99
|
+
only real cost is mixing them, the rule binds to a slot in `rules.yml → module_overlay_slots` -
|
|
100
|
+
service-method naming, the navigation-exit spelling, the component directory's name. An **unbound
|
|
101
|
+
slot disables its rules**, and the audit says so rather than defaulting to one dialect silently.
|
|
102
|
+
This matters because the alternative is reporting a hundred findings against a module that made the
|
|
103
|
+
other defensible choice, which is a migration proposal wearing a standards pass. A slot is only
|
|
104
|
+
legitimate when both values are genuinely defensible: `UI-04` is the counter-example - a module with
|
|
105
|
+
no copy surface has not chosen differently, it is missing the surface, so that rule is never slotted.
|
|
67
106
|
|
|
68
107
|
`references/swiftlint.draft.yml` carries one placeholder worth changing per project: the remote
|
|
69
108
|
image-host allowlist is set to `example.com`. Point it at your own CDN, or the rule
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
########################################################################
|
|
2
|
+
# Module overlay template.
|
|
3
|
+
#
|
|
4
|
+
# Copy to `modules/<YourModule>.yml` and fill in from EVIDENCE IN YOUR OWN CODE.
|
|
5
|
+
# A real overlay is never shipped with this skill: it quotes module paths, symbol
|
|
6
|
+
# names and call-site counts, so it describes one codebase and would bind another
|
|
7
|
+
# codebase's slots to the wrong dialect.
|
|
8
|
+
#
|
|
9
|
+
# The rule that makes this safe: an UNBOUND slot DISABLES its rules. Leaving a
|
|
10
|
+
# slot empty costs you those checks and the audit says so. Guessing a value costs
|
|
11
|
+
# you a hundred findings against a module that made the other defensible choice,
|
|
12
|
+
# which is a migration proposal wearing a standards pass. Empty beats guessed.
|
|
13
|
+
#
|
|
14
|
+
# Every `*_evidence` field wants a COUNT, not a preference. The count is the only
|
|
15
|
+
# thing that distinguishes "this module chose a dialect" from "this module has a
|
|
16
|
+
# defect". No count, no binding.
|
|
17
|
+
########################################################################
|
|
18
|
+
|
|
19
|
+
module: <YourModule>
|
|
20
|
+
path: <Path/To/Module>
|
|
21
|
+
role: feature # feature | common | app
|
|
22
|
+
registry_version: 1.1.0
|
|
23
|
+
updated: <YYYY-MM-DD>
|
|
24
|
+
|
|
25
|
+
description: >
|
|
26
|
+
Per-module overlay for the shared registry. Carries what code cannot state: the vocabulary
|
|
27
|
+
bindings a rule needs to generate its check, the module's dialect choices, prohibitions that
|
|
28
|
+
currently have zero instances, deliberate carve-outs, and the real verification command.
|
|
29
|
+
|
|
30
|
+
# --- Dialect slots -----------------------------------------------------
|
|
31
|
+
# Bind only what you can evidence. See `rules.yml -> module_overlay_slots`
|
|
32
|
+
# for what each slot governs and which values are defensible.
|
|
33
|
+
dialect:
|
|
34
|
+
# Governs SVC-07. Values: send-path | domain-verb
|
|
35
|
+
ServiceNamingScheme:
|
|
36
|
+
ServiceNamingScheme_evidence: >
|
|
37
|
+
Count both spellings before binding, e.g. "N `func send<Path>` against M `func fetch<X>`".
|
|
38
|
+
The minority spelling is then the finding this rule should report. If the counts are close,
|
|
39
|
+
the module has not chosen - leave the slot unbound and raise the split as the finding.
|
|
40
|
+
|
|
41
|
+
# Governs NAME-01. Values: coordinator-event | output-closure
|
|
42
|
+
NavExitShape:
|
|
43
|
+
NavExitShape_evidence: >
|
|
44
|
+
Count typed event enums against output closures, and name the handler symbol the coordinator
|
|
45
|
+
applies.
|
|
46
|
+
|
|
47
|
+
# Governs READ-04b. Any consistent directory name; two names in one module is the violation.
|
|
48
|
+
ComponentsDir:
|
|
49
|
+
|
|
50
|
+
# --- Vocabulary --------------------------------------------------------
|
|
51
|
+
# Symbol and path slots the judgement rules need in order to generate a check.
|
|
52
|
+
# An unfilled slot disables the rules that read it.
|
|
53
|
+
vocabulary:
|
|
54
|
+
ScreenRoot: <Sources/<Module>/Screens>
|
|
55
|
+
ScreenRoleSuffixes: [] # e.g. [Scene, ViewModel, UseCase, Repository, Mapper]
|
|
56
|
+
HandlerName: "<Screen><Suffix>"
|
|
57
|
+
|
|
58
|
+
# --- Sensitive data inventory -----------------------------------------
|
|
59
|
+
# Drives SEC-01 in both directions: a sensitive value over-persisted, AND a
|
|
60
|
+
# transient value persisted at all. Classify every one the module handles;
|
|
61
|
+
# see `rules.yml -> sensitive_data_classes` for the classes and their at-rest
|
|
62
|
+
# and loggable rules. An unclassified symbol cannot be checked.
|
|
63
|
+
sensitive_data: []
|
|
64
|
+
|
|
65
|
+
# --- Zero-instance prohibitions ---------------------------------------
|
|
66
|
+
# Shapes this module has never contained and intends never to. Recording the
|
|
67
|
+
# zero is what makes the first instance a regression instead of a debate.
|
|
68
|
+
prohibitions: []
|
|
69
|
+
|
|
70
|
+
# --- Carve-outs --------------------------------------------------------
|
|
71
|
+
# Deliberate deviations, each with a rule ID and a reason. A carve-out here is
|
|
72
|
+
# a decision; the same deviation unmarked in code is a finding. In-code form:
|
|
73
|
+
# // standard:exception(<RULE-ID>) <reason> <expiry:YYYY-MM-DD>
|
|
74
|
+
carve_outs: []
|
|
75
|
+
|
|
76
|
+
# --- Verification ------------------------------------------------------
|
|
77
|
+
# The command that actually proves this module builds and its tests pass.
|
|
78
|
+
# The audit runs this rather than assuming a scheme name.
|
|
79
|
+
validation:
|
|
80
|
+
command:
|
|
81
|
+
notes: >
|
|
82
|
+
Name the real scheme/target. A wrong command that exits 0 is worse than no command.
|
|
@@ -14,14 +14,17 @@ flexibility · consistency.**
|
|
|
14
14
|
|
|
15
15
|
1. Sensitive data goes in the Keychain, never in `UserDefaults`, and never into a log. `[SEC-01, SEC-03]`
|
|
16
16
|
2. Never reach for the environment - inject time, storage, randomness, session. `[TEST-01]`
|
|
17
|
-
3. One type per file;
|
|
17
|
+
3. One type per file, named after it; split its concerns with `// MARK:` - business rules, service
|
|
18
|
+
calls and UI never share a section. `[STRUCT-01, READ-01]`
|
|
18
19
|
4. A screen is a known set of files, always the same set. `[STRUCT-02]`
|
|
19
20
|
5. Where a type lives is decided by how many things use it. `[STRUCT-05]`
|
|
20
21
|
6. A feature module never imports another feature module. `[MOD-01]`
|
|
21
22
|
7. Everything is `private` and `final` until something forces otherwise. `[VIS-01, VIS-02]`
|
|
22
|
-
8. One request in, one
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
8. One request in, one outcome out - `async`, no completion handlers, no `throws` beside a result,
|
|
24
|
+
and the outcome is one closed enum the caller must handle exhaustively. `[SVC-01, SVC-09]`
|
|
25
|
+
9. A business rule has a name and a home a test can reach without a view. `[RULE-01, SVC-08]`
|
|
26
|
+
10. Variants are configuration, not `if` trees - and a screen's load state is one enum carrying its
|
|
27
|
+
payload, not four flags that permit impossible combinations. `[FLEX-02, NAME-06]`
|
|
25
28
|
|
|
26
29
|
---
|
|
27
30
|
|
|
@@ -101,6 +104,16 @@ private means a forgotten annotation fails safe; defaulting to public means it f
|
|
|
101
104
|
not cached to disk by default. `[SEC-05]`
|
|
102
105
|
- Analytics events, user properties and crash breadcrumbs are redacted - check the parameter
|
|
103
106
|
list of every event you add. `[SEC-06]`
|
|
107
|
+
|
|
108
|
+
Redaction at the call site is a habit, not a mechanism: it works today and fails on the event
|
|
109
|
+
somebody adds next month. Three properties make it real. **One named helper** does the reduction,
|
|
110
|
+
so there is a single implementation to review and one thing to grep. **The typed event declares
|
|
111
|
+
only the reduced form** - a parameter that *can* hold the raw value eventually does. **And the
|
|
112
|
+
reduction has a test:** deterministic for the same input, different for different inputs, and the
|
|
113
|
+
output does not contain the input. Three cheap assertions that turn a claim into a property; an
|
|
114
|
+
untested reduction helper is a finding even when the code is correct. Truncation needs one extra
|
|
115
|
+
check hashing does not - that what remains cannot identify the subject alone, and cannot be joined
|
|
116
|
+
against another field in the same payload to do so.
|
|
104
117
|
- Permissions are least-privilege with honest purpose strings; the privacy manifest matches what
|
|
105
118
|
you actually collect. `[SEC-07, SEC-08]`
|
|
106
119
|
- Debug menus, mock launch arguments and redirect shortcuts are **compiled out** of release, not
|
|
@@ -157,9 +170,56 @@ func submitGate(for passengers: [Passenger], isLoggedIn: Bool) -> SubmitGate {
|
|
|
157
170
|
needs a session, a network stub and a view model instance to answer "what happens when a document
|
|
158
171
|
is missing".
|
|
159
172
|
|
|
160
|
-
Also: name test doubles for what they do - stub, spy, fake, mock - one kind per file,
|
|
161
|
-
the signature identical to the real type, because a drifted double is the first thing the
|
|
162
|
-
person copies.
|
|
173
|
+
Also: name test doubles for what they do - stub, spy, fake, mock, **builder** - one kind per file,
|
|
174
|
+
and keep the signature identical to the real type, because a drifted double is the first thing the
|
|
175
|
+
next person copies. A builder is the one most often missing: a base value plus chainable
|
|
176
|
+
single-field overrides, so each test states only what it varies instead of restating a fifteen-field
|
|
177
|
+
initialiser. `[TEST-04, SVC-02]`
|
|
178
|
+
|
|
179
|
+
### Give the rule a name and a home `[RULE-01]`
|
|
180
|
+
|
|
181
|
+
Three homes, and the ladder picks. **One screen asks it** → the view model; stop there, a namespace
|
|
182
|
+
with one consumer is over-hoisting. **Several screens ask it of data the entity already carries** →
|
|
183
|
+
a computed property on the entity, under `// MARK: - Derived`. **Several screens ask it but the
|
|
184
|
+
answer is screen policy rather than entity state** → a named rule namespace.
|
|
185
|
+
|
|
186
|
+
```swift
|
|
187
|
+
// ✗ a private computed property on the scene; the next screen needing this re-derives it
|
|
188
|
+
private var showsStrikethrough: Bool { viewModel.segment.historyInfo?.crossed == true }
|
|
189
|
+
```
|
|
190
|
+
```swift
|
|
191
|
+
// ✓ a caseless enum of pure statics in the domain layer, each citing what it enforces
|
|
192
|
+
enum ReservationRules {
|
|
193
|
+
/// BR-21: the overline strikes ONLY when `historyInfo.crossed` is true. When it is false the
|
|
194
|
+
/// history fields may still be populated (voluntary itinerary metadata) - do not strike then.
|
|
195
|
+
static func shouldRenderStrikethrough(_ segment: Segment) -> Bool {
|
|
196
|
+
segment.historyInfo?.crossed == true
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
```swift
|
|
201
|
+
// ✓ and the test repeats the citation token, so one grep reaches spec, code and test
|
|
202
|
+
@Test("BR_21: no strikethrough when the segment is not crossed")
|
|
203
|
+
func test_BR_21_noStrikethrough_whenNotCrossed() { ... }
|
|
204
|
+
```
|
|
205
|
+
**Why:** the citation is not decoration, it is the rule's own measurement. A namespace whose
|
|
206
|
+
functions cite nothing cannot be checked against anything, and a cited requirement with no test of
|
|
207
|
+
the same token is an untested requirement. What the namespace must NOT become is a Utils bucket: a
|
|
208
|
+
value transform is `READ-04d`, a shape lowering is `SVC-08`, a form rule is `STRUCT-06`.
|
|
209
|
+
|
|
210
|
+
### A null object may ship; a test double may not `[TEST-08]`
|
|
211
|
+
|
|
212
|
+
A type that satisfies a protocol by doing nothing - no event sent, `nil` returned, empty list - is
|
|
213
|
+
the honest default for a dependency a caller has legitimately not wired: a defaulted initialiser
|
|
214
|
+
parameter, a canvas preview, an optional slot nobody configured yet. Name it consistently and let it
|
|
215
|
+
live in production sources.
|
|
216
|
+
|
|
217
|
+
A type that carries **canned payloads** does not. It ships sample data to users, grows the store
|
|
218
|
+
binary, and can be resolved by accident because nothing but its name says it is not real. It belongs
|
|
219
|
+
in the test target. Distinguish by payload, never by name: a `Mock` that returns nothing is a null
|
|
220
|
+
object misnamed, and a `Noop` returning three sample records is a double in the wrong target. If a
|
|
221
|
+
canned payload really is needed at runtime - a demo build, an offline mode - that is a debug
|
|
222
|
+
affordance and `SEC-09` asks what removes it from the store build.
|
|
163
223
|
|
|
164
224
|
---
|
|
165
225
|
|
|
@@ -289,6 +349,14 @@ func fetchAreaCodeList() async -> Result // standard:exception(SVC-07) scree
|
|
|
289
349
|
tells you which service fires without opening the repository. It also survives the rename the
|
|
290
350
|
other way round: when the backend renames an endpoint, the compiler shows you every screen.
|
|
291
351
|
|
|
352
|
+
**This one is a module decision, not a universal.** The alternative - naming the method after what
|
|
353
|
+
the domain asks for - reads better at the call site and survives an endpoint rename, at the cost of
|
|
354
|
+
that grep. Both are defensible, so the rule binds to the `ServiceNamingScheme` overlay slot: it is
|
|
355
|
+
active only where a module declared `send-path`, and what it then enforces is consistency with the
|
|
356
|
+
module's own declared scheme, never conformity to a sibling's choice. Check the module's overlay
|
|
357
|
+
before raising this. Same for the navigation-exit spelling `[NAME-01]` and the component directory's
|
|
358
|
+
name `[READ-04b]` - see `rules.yml → module_overlay_slots`.
|
|
359
|
+
|
|
292
360
|
### A mapper moves values; it never decides `[SVC-08]`
|
|
293
361
|
|
|
294
362
|
```swift
|
|
@@ -319,6 +387,13 @@ from the view model that owns the behaviour, untestable without hand-building a
|
|
|
319
387
|
duplicated the next time another screen needs the same decision. `?? ""` on an optional wire
|
|
320
388
|
field is not a decision - it is the lowering itself.
|
|
321
389
|
|
|
390
|
+
**Moving a rule out of a mapper is two edits, not one.** The reason these survive review is that
|
|
391
|
+
deleting the decision also deletes the data it was computed from: the mapper stored `isFailure` and
|
|
392
|
+
dropped `info.status`, so no later layer can re-derive it. First make the entity carry the raw
|
|
393
|
+
inputs, then express the decision - and put it in one of the three homes `[RULE-01]` names, never as
|
|
394
|
+
a stored field the mapper fills, because a stored field is indistinguishable from a wire value at
|
|
395
|
+
every call site that reads it.
|
|
396
|
+
|
|
322
397
|
### Signatures read as the contract `[SVC-01]`
|
|
323
398
|
|
|
324
399
|
```swift
|
|
@@ -336,6 +411,75 @@ public func savePassengers(
|
|
|
336
411
|
result family means one error channel instead of `throws` plus a result plus an optional. Past
|
|
337
412
|
~2 parameters at a service boundary, the parameters want to be a model.
|
|
338
413
|
|
|
414
|
+
### The outcome is a closed enum `[SVC-09, SVC-05]`
|
|
415
|
+
|
|
416
|
+
```swift
|
|
417
|
+
// ✗ two outcome channels, so no switch over the result is ever exhaustive
|
|
418
|
+
func checkAccess(identifier: String) async throws -> AccessResponse
|
|
419
|
+
```
|
|
420
|
+
```swift
|
|
421
|
+
// ✓ one type enumerating every outcome the caller must handle
|
|
422
|
+
enum AccessOutcome: Equatable, Sendable {
|
|
423
|
+
case dashboard(pnr: String?) // a success case may name a DESTINATION
|
|
424
|
+
case ticketEntrance(pnr: String?)
|
|
425
|
+
case serviceError(referenceCode: String?, message: String?)
|
|
426
|
+
case offline
|
|
427
|
+
case timeout
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ✓ and the transport error is projected in the data layer, never above it
|
|
431
|
+
} catch let error as ServiceError {
|
|
432
|
+
return error.projected(offline: .offline, timeout: .timeout) {
|
|
433
|
+
.serviceError(referenceCode: $0, message: $1)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
**Why:** the compiler answers "is any outcome unhandled?", which no `catch` can, and a success case
|
|
438
|
+
naming a destination is what this buys over a bare `Result<Payload, Error>` - the wire's redirect
|
|
439
|
+
string is resolved into a case in the repository, so no raw string reaches the view model.
|
|
440
|
+
|
|
441
|
+
**Where the line falls between this and `SVC-05`.** How a call can FAIL is a module fact: one shared
|
|
442
|
+
failure vocabulary, one factory that projects the transport error onto it. What a screen does NEXT is
|
|
443
|
+
a screen fact: its own outcome enum, with its own destinations. A screen enum is correct when its
|
|
444
|
+
failure cases name the shared kinds; it is the violation when it respells them. Re-declaring the same
|
|
445
|
+
offline / timeout / server triple in a dozen screen enums means a new failure kind is a dozen edits
|
|
446
|
+
and the classifications drift apart - which is the `SVC-05` finding, not an argument against this
|
|
447
|
+
shape.
|
|
448
|
+
|
|
449
|
+
### The load state is one enum with its payload attached `[NAME-06]`
|
|
450
|
+
|
|
451
|
+
```swift
|
|
452
|
+
// ✗ four properties; `isLoading` with data present and an error set is a state nothing rejects
|
|
453
|
+
var isLoading = false
|
|
454
|
+
var reservation: Reservation?
|
|
455
|
+
var errorMessage: String?
|
|
456
|
+
var isOffline = false
|
|
457
|
+
```
|
|
458
|
+
```swift
|
|
459
|
+
// ✓ the payload hangs off the case, and the enum answers questions so the view never destructures
|
|
460
|
+
struct LoadedContent: Hashable {
|
|
461
|
+
let reservation: Reservation
|
|
462
|
+
/// `nil` when the passenger request failed but the reservation loaded - the screen still
|
|
463
|
+
/// renders its flights, the passenger area is simply not drawn.
|
|
464
|
+
let passengerArea: PassengerArea?
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
enum ScreenState: Hashable {
|
|
468
|
+
case idle, loading
|
|
469
|
+
case loaded(LoadedContent)
|
|
470
|
+
case error(ScreenError)
|
|
471
|
+
|
|
472
|
+
var isLoading: Bool { if case .loading = self { return true }; return false }
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
Two more things to copy. **State the case you deliberately did not model, in the file:** "there is no
|
|
476
|
+
`empty` case - a successful response always represents a populated record; an empty payload arrives
|
|
477
|
+
as a not-found error." Without that line the next reader cannot tell a considered omission from an
|
|
478
|
+
oversight and adds the case defensively. And **a secondary source that may fail is an optional slot
|
|
479
|
+
inside the payload, not a second state** - say what `nil` means at the property. Promoting a
|
|
480
|
+
non-critical failure to a screen-level error discards the data that did arrive; silently defaulting
|
|
481
|
+
it to an empty value makes "absent" and "empty" indistinguishable.
|
|
482
|
+
|
|
339
483
|
### Visibility is documentation `[VIS-01, VIS-02, VIS-04]`
|
|
340
484
|
|
|
341
485
|
Everything `private` and `final` until something forces otherwise. `public` only on what another
|
|
@@ -349,11 +493,26 @@ implementation detail explains itself through naming, a contract between two tea
|
|
|
349
493
|
|
|
350
494
|
### A screen is a known file manifest `[STRUCT-02, STRUCT-03]`
|
|
351
495
|
|
|
352
|
-
`Scene · ViewModel ·
|
|
353
|
-
(+protocol
|
|
354
|
-
does not. An empty
|
|
496
|
+
`Scene · ViewModel · LoadState · CopySurface · NavigationExit · AnalyticsTracking · UseCase ·
|
|
497
|
+
Repository (+protocol) · Mapper + models` - each present when its responsibility exists, absent when
|
|
498
|
+
it does not. An empty copy surface on a screen with no copy is noise, not compliance. Every screen
|
|
355
499
|
sits at the same depth with the same internal grouping, because people navigate by muscle memory.
|
|
356
500
|
|
|
501
|
+
Two of those names are the module's to spell. The navigation-exit type is a `CoordinatorEvent` enum
|
|
502
|
+
with a handler alias in some modules and an `Output` enum with a closure in others - both enumerate
|
|
503
|
+
every exit in one place, which is the property that matters `[NAME-01]`. The test double is not on
|
|
504
|
+
the list at all: it lives in the test target `[TEST-08]`.
|
|
505
|
+
|
|
506
|
+
**Where the protocol/implementation split earns its keep `[STRUCT-08]`.** An implementation with a
|
|
507
|
+
body - a repository doing request construction, mapping, cache reads, error projection - goes in its
|
|
508
|
+
own file beside its protocol, so a reader opening the contract does not scroll past it. A use case
|
|
509
|
+
that is a pure pass-through, forwarding one call to one collaborator, may keep protocol, live type
|
|
510
|
+
and null object in one file: there is nothing to scroll past, and splitting forty of those produces
|
|
511
|
+
eighty files whose names differ by a suffix. Decide by whether there is anything a reader must skip,
|
|
512
|
+
never by the layer's name - and answer the same way across the module, because the cost of this rule
|
|
513
|
+
is a reader guessing which file a type is in. (A pass-through use case also invites `SVC-01`'s
|
|
514
|
+
question: what does it add over calling the repository? Separate finding.)
|
|
515
|
+
|
|
357
516
|
### Placement is a consumer count `[STRUCT-05]`
|
|
358
517
|
|
|
359
518
|
| Consumers | Home |
|
|
@@ -16,7 +16,19 @@
|
|
|
16
16
|
set -euo pipefail
|
|
17
17
|
|
|
18
18
|
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
|
-
|
|
19
|
+
# Scratch root. `~/.claude` is one host's tree: defaulting there made a Copilot- or
|
|
20
|
+
# Codex-only machine create a stray .claude directory just to hold lint output. Prefer
|
|
21
|
+
# whichever host tree already exists, and fall back to a neutral location.
|
|
22
|
+
if [ -n "${IOS_STANDARD_WORK_ROOT:-}" ]; then
|
|
23
|
+
WORK_ROOT="$IOS_STANDARD_WORK_ROOT"
|
|
24
|
+
else
|
|
25
|
+
WORK_ROOT=""
|
|
26
|
+
for _host_dir in "$HOME/.claude" "$HOME/.copilot" "$HOME/.codex"; do
|
|
27
|
+
[ -d "$_host_dir" ] && { WORK_ROOT="$_host_dir/local/ios-coding-standard"; break; }
|
|
28
|
+
done
|
|
29
|
+
unset _host_dir
|
|
30
|
+
[ -n "$WORK_ROOT" ] || WORK_ROOT="${TMPDIR:-/tmp}/ios-coding-standard"
|
|
31
|
+
fi
|
|
20
32
|
DRAFT_CONFIG="$SKILL_DIR/swiftlint.draft.yml"
|
|
21
33
|
|
|
22
34
|
MODULE_PATH="${1:-}"
|