apsis-rs/docs/design/layers.md

108 lines
4.0 KiB
Markdown

# Layer boundaries
English | [简体中文](./layers.zh.md)
This file records the layering rule of the workspace and why certain
cross-layer dependencies are allowed. The "Layers" section of the README is
the rule itself; what belongs here is how it is decided and where it has edge
cases.
## The rule
The layers run left to right:
```text
foundation -> physics -> orbit -> rf -> network -> ops -> sim -> apsis-rs
```
A dependency edge runs from crate A to crate B. Map the layer of A and the
layer of B to indices, and the rule is:
```text
index(B) <= index(A) allowed
index(B) > index(A) a violation, a reverse dependency
```
In other words, **dependencies go to the left and never to the right.**
`scripts/check-layers.sh` applies this to every manifest in the workspace,
and CI runs it.
## Crossing layers is allowed
The rule constrains direction, not distance. `network` may depend on `orbit`
even though `rf` sits between them, because `orbit` is to the left of
`network` and the direction is correct.
Allowing a leftward crossing is deliberate. Forbidding it would create two
kinds of problem:
- **There may be no type to carry the value.** When a position from the
`orbit` layer is handed to the `network` layer, the `rf` layer need not
offer a type that carries it. Requiring the trip through `rf` would only
produce a shell type that forwards.
- **It would be coupling without need.** The constellation calculation in
`network` wants the Keplerian elements from `apsis-kepler` and has no use
for `rf`. Forbidding the crossing would make `network` depend on `rf` only
to reach a type it does not use.
The rule therefore constrains **direction**, because direction is what
carries the meaning of layering: a concept of a higher layer may refer to a
concept of a lower one, and the other way round would mean the layering is
wrong.
## The global baselines
`apsis-core` (the general error, the constants, the unit marker) and
`apsis-math` (`Vec3`, `Mat3`, `Quat`) are shared by every layer. They sit in
`foundation` themselves, so "to the left only" already lets any layer use
them and no special case is needed.
This differs from a within-layer baseline. `apsis-core` is also the baseline
inside `foundation`, but its reach is the whole workspace rather than one
layer.
**Declare a dependency only where it is used.** Any crate may use
`apsis-core` and `apsis-math`, but a crate that does not reference them
should not list them: an unused dependency slows the build, adds noise to
`cargo tree` and to the layer check, and misstates who depends on what.
Adding one later is a single line whenever it is needed. The edge count that
`scripts/check-layers.sh` reports is therefore a real usage relation rather
than a list of intentions.
## The facade within a layer
Each layer has one facade crate as its integration point:
| Layer | Facade |
| --- | --- |
| orbit | `apsis-orbit` |
| rf | `apsis-rf` |
| sim | `apsis-sim` |
| outward | `apsis-rs` |
A facade combines the capabilities of its layer into a few types and
re-exports the remaining crates of the same layer. `foundation`, `physics`,
`network` and `ops` have no separate facade: their crates are each useful on
their own, and `apsis-rs` re-exports them together.
## Edge cases
- **The `apsis-rs` facade is not in the layer sequence.** It depends on
every layer, because it is the exit. `scripts/check-layers.sh` decides by
the layer directory name, and `crates/apsis-rs` does not have the shape
`crates/<layer>/<crate>`, so it is skipped.
- **Dev-dependencies count.** A reverse dependency in a test shows the
layering is just as wrong there, so the fix is to change the code rather
than relax the test.
- **A facade re-export is not a new dependency.** `apsis-rs` re-exporting
`apsis-orbit` is what a facade does, not a layer reaching into `orbit`.
## Checks
```bash
bash scripts/check-layers.sh
```
The script prints the number of dependency edges it checked, and lists each
reverse dependency before exiting with status one if it found any.