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.