SLD8xx - Cross-File Contracts, Documentation, and Architecture¶
The 800-series checks are split into three groups that each require a workspace-wide view: cross-file public contracts (SLD80x), documentation symmetry (SLD81x / SLD82x), and the runtime import graph (SLD83x).
SLD80x - Cross-File Public Contracts¶
SLD80x are cross-file checks: they require a workspace-wide view and
therefore do not run as part of the flake8 plugin. They are emitted by the
python -m stolid runner, which scans every .py file under the
given paths (honoring .gitignore) and then re-walks each file to
classify every public annotation against the workspace symbol table.
A public annotation may only name a contract (Protocol, ABC,
TypedDict, NamedTuple, Enum, or a data-only @dataclass),
a primitive (int, str, bool, …), or compose those through
an abstract container from collections.abc / typing (Mapping,
Sequence, Iterable, …) or a union. Naming an open concrete
class commits callers to that exact type; the rule rejects it so that
implementations stay swappable behind their contracts.
A “data-only @dataclass” is a @dataclass-decorated class whose
body declares only public annotated fields (optionally with a
docstring): no methods, no private (_-prefixed) fields. Such a class
is a pure record – its annotations are its contract – so callers may
safely depend on it, assuming the field types are themselves allowed.
“Public” surface here is decided by name, not by the module a
definition lives in: top-level functions, public classes and their
public methods, and public class-level fields whose names do not start
with a single underscore. A public-named definition is on the contract
surface even inside a private (``_``-prefixed) module — callers can
still import and depend on it, so its annotations are still a commitment.
Only private members (_-prefixed functions, classes, fields, and
members nested in public classes) are skipped.
This is deliberately stricter than the module-based privacy used by the
documentation checks below (SLD81x / SLD82x): a private module exempts
its members from docstring requirements, but not from the public
contract. If a function in a private module should be free to name
concrete types, give it a _-prefixed name so it is genuinely
private.
Diagnostics honor per-line # noqa markers exactly like SLD801.
SLD802: Public annotation references a workspace-defined concrete
class. A concrete class is any class X: that is not a Protocol, ABC,
TypedDict, NamedTuple, Enum, or data-only @dataclass. Names that are
not defined anywhere in the scanned workspace are treated as
out-of-scope third-party references and are silently allowed.
# Bad: backend.py
from dataclasses import dataclass
@dataclass(frozen=True, slots=True, kw_only=True)
class RealBackend:
name: str
def fetch(self, key: str) -> bytes: ...
# api.py
from .backend import RealBackend
def run(b: RealBackend) -> None: ...
# Good: backend.py
from typing import Protocol
class Backend(Protocol):
def fetch(self, key: str) -> bytes: ...
# api.py
from .backend import Backend
def run(b: Backend) -> None: ...
SLD803: Public annotation uses a concrete builtin container
(list, dict, set, frozenset). Use Mapping,
Sequence, AbstractSet, Iterable, or another abstract from
collections.abc / typing – callers should commit to the
operations they need, not to the concrete container that produces them.
# Bad
def collect(items: list[str], counts: dict[str, int]) -> set[str]: ...
# Good
from typing import Iterable, Mapping, AbstractSet
def collect(
items: Iterable[str], counts: Mapping[str, int]
) -> AbstractSet[str]: ...
SLD804: Public annotation uses a variadic tuple (tuple[X, ...]).
A variadic tuple is a homogeneous, indefinitely-long sequence; express
that with Sequence[X] or Iterable[X]. Fixed-arity tuples
(tuple[int, str]) are fine – they describe a precise structure.
# Bad
def pack(xs: tuple[int, ...]) -> None: ...
# Good
from typing import Sequence
def pack(xs: Sequence[int]) -> None: ...
SLD81x / SLD82x - Documentation¶
stolid takes a strict, symmetric view of docstrings:
Public surfaces must carry a docstring (SLD81x).
Private surfaces must not carry a docstring — implementation notes belong in
#comments (SLD82x).
A module is public when its filename does not start with a single
underscore (__init__.py is treated as public because the package
itself usually is). A class, function, or method is public when its
name does not start with an underscore. Dunder names (__str__,
__eq__, …) are exempt from both policies: they implement
protocols, not API. Inner functions — functions defined inside another
function — are also exempt, regardless of name. The content checks
(SLD814/SLD815/SLD816) apply only once a docstring is present.
SLD811: Public module missing a module docstring. Add a top-level string literal as the first statement.
# Bad
import os
x = 1
# Good
"""Brief description of this module."""
import os
x = 1
SLD812: Public class missing a docstring (applies even when the class lives in a private module).
# Bad
class Reporter:
...
# Good
class Reporter:
"""Render diagnostic lines for the duplicate scanner."""
SLD813: Public function or method missing a docstring (applies even inside a private module or private class). Inner functions never need one.
# Bad
def parse(source):
...
# Good
def parse(source):
"""Parse ``source`` and return an AST."""
...
SLD814: Function docstring does not mention an argument by name.
Types are documented via mypy; the docstring describes the semantics of
each parameter. self and cls are exempt.
# Bad
def add(a: int, b: int) -> int:
"""Add things and return the result."""
return a + b
# Good
def add(a: int, b: int) -> int:
"""Return the sum of ``a`` and ``b``."""
return a + b
SLD815: Function docstring does not mention the return value. The
docstring must contain one of return, returns, yield, or
yields (case-insensitive) unless the function is annotated to return
None (or has no return annotation).
# Bad
def first(seq: list[int]) -> int:
"""The first element of ``seq``."""
return seq[0]
# Good
def first(seq: list[int]) -> int:
"""Return the first element of ``seq``."""
return seq[0]
SLD816: Dataclass docstring does not mention a non-private field.
Document every public field by name in the class docstring, or annotate
it with field(doc=...) so its documentation lives at the field
itself. Fields whose name starts with _ are exempt.
# Bad
@dataclass(frozen=True, slots=True, kw_only=True)
class Point:
"""A 2D point."""
x: int
y: int
# Good
@dataclass(frozen=True, slots=True, kw_only=True)
class Point:
"""A 2D point with coordinates ``x`` and ``y``."""
x: int
y: int
SLD821: Private module (filename starts with _) has a module
docstring. Convert it to a top-of-file # comment block.
# Bad: _internals.py
"""Implementation details for the duplicate scanner."""
import ast
...
# Good: _internals.py
# Implementation details for the duplicate scanner.
import ast
...
SLD822: Private class has a docstring. Convert it to a # comment
immediately above the body (or before the class line).
# Bad
class _State:
"""Traversal state for the visitor."""
depth: int = 0
# Good
class _State:
# Traversal state for the visitor.
depth: int = 0
SLD823: Private function or method has a docstring. Use a #
comment instead.
# Bad
def _normalize(text):
"""Lowercase and strip ``text``."""
return text.strip().lower()
# Good
# Lowercase and strip ``text``.
def _normalize(text):
return text.strip().lower()
SLD83x - Import Graph Architecture¶
SLD83x are workspace-wide architectural checks: they require a global
view of every .py file and therefore run via python -m stolid,
not as flake8 plugin rules. They analyze the runtime import graph,
collapsing each level of the package hierarchy into a quotient graph
and applying graph-theoretic primitives (strongly connected components,
condensation depth, reach density) at each level.
Imports under if TYPE_CHECKING: (and typing.TYPE_CHECKING /
t.TYPE_CHECKING aliases) are excluded — they do not execute at
runtime and are a legitimate way to break otherwise-unavoidable cycles.
The rules measure runtime architectural structure, not the strictly
larger graph of conceptual coupling.
Per-line # noqa markers are not honored for SLD83x: these are
workspace-wide claims, not per-line, and silencing one __init__.py
would be a poor expression of “this whole architecture is fine.”
SLD831: Cyclic dependency cluster contains more than 15 modules.
Small cycles (requests has 8, click 11, flask 14) remain
comprehensible; clusters past ~15 modules begin to function as
undifferentiated mud where every module can reach every other and
refactors propagate unpredictably.
SLD832: Cross-package cyclic dependency at any level ≥ 2 whose constituent subpackages cover more than 10 modules total. This is the rule that catches the architectural failure where individual modules are arranged hierarchically but subpackages reach across each other circularly. The threshold is on per-SCC module weight: a cycle between two 100-module subpackages is much worse than a cycle between two 3-module subpackages even though both involve 2 nodes at the quotient level.
SLD833: Condensation depth exceeds 8 at any level with at least
10 nodes. The condensation depth is the smallest number of layers any
valid import-linter layered contract would need; eight is generous
— most well-organized architectures have three to five. The 10-node
minimum prevents the rule from firing on tiny levels where depth is
mechanically constrained by node count anyway. Fires once per level
that exceeds the threshold; level 4 and level 5 both firing yields
two diagnostics because each level represents an independent
architectural slice.
SLD834: At any level ≥ 2 with at least 10 nodes, the largest SCC contains more than 60% of the level’s total module weight. This rule names the architecture where the majority of code lives in one mutually-reachable cluster of subpackages — not a layered architecture, just a directory hierarchy stretched over a single component.
SLD835: Module-level reach density exceeds 0.6, where density is
the count of ordered transitively-reachable pairs plus an SCC bonus,
normalized by n * (n - 1). A fully-reachable chain scores 0.5;
fully-mutually-reachable modules score above 1.0. This rule catches
the small-package failure mode: most modules importing most modules
transitively, just without the structure being large enough to
trigger SLD831 or to have a meaningful level ≥ 2 for SLD832/SLD834.