---
name: ios-module-structure
description: "The ios-module-structure rule registry: stable-ID rules over where a declaration lives, what its file is called and what its folder must contain beside it, plus a checker that runs them against one module. Use when auditing or refactoring a module's tree, or when a review wants a rule ID rather than a preference about layout."
user-invocable: true
standards-registry: references/rules.yml
---

# Module structure

The registry is `references/rules.yml`. Sibling to the coding-standard registry: that one governs
what is inside a file, this one governs the tree - which folder a declaration lives in, what its
file is called, and what has to sit beside it.

| File | What it is | When to read it |
|---|---|---|
| `references/rules.yml` | the rules, with stable IDs, severity, enforcement kind and the predicate each one runs | before asserting that a layout is or is not a violation |
| `scripts/check_structure.py` | the checker: resolves the module's bindings, runs the predicates, reports by ID | every audit; never eyeball a tree you can measure |
| `modules/<Module>.yml` | per-module overlay: role bindings, dialect choices, vocabulary, carve-outs. **Project-local, never shipped** - it lives in the installed copy, never in the authoring source | before auditing a module, and before raising any slot-bound rule |
| `modules/_TEMPLATE.yml` | the overlay's shape and the evidence each binding wants | when onboarding a module |

**Maturity: 0.x, iOS/Swift only.** The registry declares `scope.languages`, and the checker
refuses a module that holds none of that language rather than reporting a clean run. A second
language would be a second rules file passed to `--rules`, not a change to the checker.

## The one idea

A rule here never names a path or a type. It names a **role**, and the module's overlay binds that
role to its own spelling. `STRUCT-07` does not say "a file under Subviews/ needs a Configuration
beside it"; it says every file playing `subview.view` has the `subview.configuration` file beside
it. A module that spells those two differently rebinds; a module that has no such shape leaves the
role empty and the rule switches off.

That is what lets one registry serve two modules whose trees share almost nothing, without either
becoming a hundred findings.

## How to use it

1. **Cite, do not paraphrase.** Reference `STRUCT-04`, `VOCAB-01`. A finding without an ID is a
   preference about folders, and the author cannot look it up to disagree.
2. **A rule not in the registry is not a rule.** If an audit wants one, propose it as
   `status: proposed` with a rationale rather than enforcing it silently.
3. **An unbound slot or role DISABLES its rules, and the run reports that.** Never read a clean run
   without reading its `DISABLED` block - that block is the coverage you did not have.
4. **Severity decides whether it blocks.** `blocking` stops the change, `important` is fixed in the
   same pass, `suggestion` is optional.
5. **Exceptions are marked in code, with an expiry:**
   `// standard:exception(<RULE-ID>) <reason> <expiry:YYYY-MM-DD>`. An unmarked deviation is a
   finding; a marked one is a decision.
6. **When a finding looks wrong, fix the overlay before the code.** A rule firing on a whole module
   is almost always a dialect the overlay has not declared. Changing the code to satisfy a
   mis-bound rule is the expensive mistake this registry exists to prevent.

## Running it

```bash
python3 "$HOME/.claude/skills/ios-module-structure/scripts/check_structure.py" \
  --rules   "$HOME/.claude/skills/ios-module-structure/references/rules.yml" \
  --overlay "$HOME/.claude/skills/ios-module-structure/modules/<Module>.yml" \
  --root    "<path/to/module>"
```

`--only <RULE-ID>` runs one rule, `--screen <Name>` one screen, `--format json` gives a machine
result. Exit is 1 when there is any finding, 0 otherwise; notes and disabled rules never affect it.

Requires PyYAML - the registry and the overlay are YAML.

## Dialect slots, and why a rule becomes one

Some layouts are a choice, not a defect: two shapes are each coherent and the cost is only in
mixing them. Those rules bind to a slot in `module_overlay_slots` and the overlay picks a value.
A slot is only legitimate when **both** values are genuinely defensible - a module with no analytics
surface has not chosen a different dialect, it is missing the surface, so that stays a rule.

The test when you are tempted to add one: can you write the second value's paragraph without it
reading as an excuse? If not, it is a rule.

## Multi-target packages

A domain that outgrows one target splits into one target per flow inside a single package, and
the tree rules then apply PER TARGET. What the shape adds, and what the overlay binds for it:

- `screen.root` binds with a leading `Sources/*/` so every target's `Screens/` is walked; a
  target with no screens simply matches nothing.
- Each screen-bearing target carries its own dependency configurator - registration, validation
  pins, and the store entry the composition root aggregates - bound as `target.configurator`
  and proven by STRUCT-21. Registration lives with the target, never pooled in a sibling.
- The flow-contracts side mirrors the split: one contracts sub-folder per target, named
  `<Module><Target>FlowContracts`-style, under the module's contracts target
  (`contracts.root` + STRUCT-18 prove each screen's pair).
- A scaffold target - empty configurator, a route enum with an empty destination switch, no
  screens yet - is a legitimate recorded state, not a finding: it reserves the flow's seams so
  the first screen lands as a fill-in, not a redesign.
- Tests mirror per target (`Tests/<Target>Tests/`), so the source/test mirror is a set of
  prefix pairs; a single-swap `source.root`/`test.root` binding cannot express it and stays
  unbound until the checker learns per-target templates.

