INW018 diagram-forbidden-edge¶
What it does¶
Opt-in, and a warning when on. Reads the marked Mermaid and PlantUML diagrams in the files the diagrams key lists, as INW017 does, and reports an arrow that draws an import [tool.inwards] forbids, on the diagram's own line. A whole-project run checks them all; the per-edit hook, the editor and MCP check_files check a listed diagram when it is the file edited or named.
A solid arrow that points one way (-->, ==>, --->, -- text -->) means "may import": the node at the tail may import the node at the head. What is reported:
- In a
%% inwards: layersdiagram, an arrow that points outward, from an inner layer to an outer one, or between two sibling layers of one rank (a nested array inlayers, ora | bin a template's roles). - In a
%% inwards: contextsdiagram, an arrow from one context into another that the first doesn't list independs-on. When it does, an arrow that points at a node inside the second context whose quoted label isn't one of itspublicmodules.
A node in a contexts diagram belongs to the top-level subgraph it is first written in, or is its own context when it sits outside every subgraph. Each forbidden pair is reported once per diagram, at its first arrow.
Left alone: dotted (-.->), invisible (~~~), open (---) and two-way (<-->) links; nodes with the class external; arrows inside one layer or context; an end that names no declared layer or context (INW017 reports it); an arrow into an unlabelled node inside a context, whose module the diagram doesn't say.
Why is this bad¶
A diagram that shows domain --> infrastructure tells every reader, and every agent that reads the docs before it writes code, that the domain may import infrastructure. The checks reject that import, so whoever follows the picture writes code that fails INW001, INW002 or INW003. Or the picture is right and the config is behind, and nobody knows which. Either way the docs and the rules disagree about the architecture.
[tool.inwards] stays the only source of truth (ADR-045): the arrows are checked against it, never read as config. INW017 checks the names in a diagram; INW018 checks its arrows.
Example¶
[tool.inwards]
diagrams = ["docs/*.mmd"]
layers = [
{ name = "domain", modules = ["shop.domain"] },
{ name = "application", modules = ["shop.application"] },
{ name = "infrastructure", modules = ["shop.infrastructure"] },
]
[tool.inwards.rules]
extend-select = ["INW018"]
A diagram that draws the domain importing the infrastructure:
%% inwards: layers
flowchart LR
infrastructure --> application --> domain
domain --> infrastructure
docs/layers.mmd:4:3: INW018 (warning) "domain --> infrastructure" draws an import [tool.inwards] forbids: "domain" is inner to "infrastructure", and an inner layer may not import an outer one.
fix: Turn the arrow around or remove it: "infrastructure --> domain" is the direction [tool.inwards] allows.
1. Arrows point from the importing layer to the imported one, so they point inward.
2. If the arrow shows a call at run time or a flow of data rather than an import, draw it dotted (-.->), which INW018 doesn't read.
3. If the code really needs this import, ask the user whether [tool.inwards] should allow it; don't edit [tool.inwards] yourself.
docs: https://sircypkowskyy.github.io/inwards/rules/INW018/
Fixed: the arrow shows what it stands for, the infrastructure calling back into the domain at run time, so it is dotted. The whole project passes:
%% inwards: layers
flowchart LR
infrastructure --> application --> domain
domain -. events .-> infrastructure
In a contexts diagram, with sales depending on catalog and catalog publishing shop.catalog.api, the first arrow is fine and the other two are reported:
[[tool.inwards.contexts]]
name = "sales"
modules = ["shop.sales"]
depends-on = ["catalog"]
[[tool.inwards.contexts]]
name = "catalog"
modules = ["shop.catalog"]
public = ["shop.catalog.api"]
%% inwards: contexts
flowchart LR
subgraph sales["shop.sales"]
checkout["shop.sales.checkout"]
end
subgraph catalog["shop.catalog"]
api["shop.catalog.api"]:::public
store["shop.catalog.store"]
end
checkout --> api %% allowed
checkout --> store %% store isn't public
catalog --> sales %% catalog doesn't depend on sales
How it works¶
The diagram is read once for INW017 and INW018 (the reader is described on INW017's page). For each one-way solid or thick arrow between two different nodes, neither of them external:
- Layers. Both ends must name a layer: the id is the layer's name, or, for a layer whose name can't be a Mermaid id (a template's
slices.domain), the node's quoted label is one of the layer'smodulesentries. The arrow is allowed when the head's rank is lower than the tail's. A layer's rank is its place inlayers, innermost first; sibling layers share one. - Contexts. Each end stands for the context of the top-level subgraph it sits in, or for itself outside every subgraph, resolved the same way as a layer. The tail's context must list the head's in
depends-on. When the head is a node inside its context (not the subgraph itself) and has a quoted label, that label must lie under one of the context'spublicentries, or be the module of an exact (=) entry, as INW003 decides for an import.
Only the diagram's own arrows are judged. An import in the code that the diagram doesn't draw is INW019's.
How to fix¶
- Turn an outward arrow around: arrows point from the importing layer to the imported one.
- Remove an arrow between sibling layers, or between contexts that don't depend on each other.
- Point an arrow into another context at one of its public modules.
- Draw an arrow that stands for something other than an import (a call at run time, events, data flow) dotted:
-.->. - If the diagram is right and the config is behind, ask the user to change
[tool.inwards]; an agent doesn't edit it.
Fix safety¶
No automatic fix (autofix: false). Only a person knows whether the picture or the config is wrong. Redrawing an arrow changes docs only; changing [tool.inwards] changes what every rule checks.
Configuration¶
Severity warning, so docs drift doesn't block the Stop gate by default. Opt-in: turn it on with extend-select, usually together with INW017, and list the files in diagrams:
[tool.inwards]
diagrams = ["README.md", "docs/**/*.md"]
[tool.inwards.rules]
extend-select = ["INW017", "INW018"]
severity = { INW018 = "error" } # optional: block on drift
The rule has no options of its own; [tool.inwards.rules.diagram-forbidden-edge] takes only modules, which has no effect, since a diagram isn't a module. A finding can't be suppressed inline: draw the link dotted or mark the node :::external instead. A diagrams entry that matches no file is reported by INW017.
inwards import-diagram writes a [tool.inwards] from a diagram that INW017 and INW018 then pass.
Known limitations¶
- Direct arrows only.
a --> b --> callowsato importbandbto importc; nothing is said aboutaandc, asdepends-onisn't transitive. - A node's module comes from its quoted label. An arrow into an unlabelled node inside a context isn't checked against
public. - The same limits as INW017: flowcharts only, one line at a time, a code edit doesn't check the diagrams, and globs skip hidden directories.
References¶
- #345: the rule and
import-diagram; #332: the design decision, recorded in ADR-045. - Mermaid: Flowcharts syntax: link kinds, subgraphs and classes.