Skip to content

symfonic.devtools.archcheck

archcheck

Architecture rule checks (T1.2.4, REQ-S1.2).

Automated dependency-direction, cycle, forbidden-import, module-size, and public-export drift checks driven by repository-owned configuration:

  • allowed dependency matrix (source of truth): .agent/team/framework-refactor/evidence/T1.2.1/dependency-matrix.md
  • checker configuration: .agent/team/framework-refactor/evidence/T1.2.4/archcheck.toml
  • exception registry (owner / rationale / expiry / review date): .agent/team/framework-refactor/evidence/T1.2.4/exception-registry.toml

Run python -m symfonic.devtools.archcheck from the repository root.

ArchcheckConfig dataclass

ArchcheckConfig(repo_root: Path, src_root: Path, matrix_path: Path, registry_path: Path, export_baseline_path: Path, layer_map: dict[str, str] = dict(), composition_roots: tuple[str, ...] = (), forbidden_kernel_imports: tuple[str, ...] = (), port_module_names: tuple[str, ...] = ('ports', 'protocol', 'protocols'), max_module_lines: int = 300, public_export_modules: tuple[str, ...] = ())

layer_of

layer_of(module: str) -> str | None

Longest-pattern layer lookup for a dotted module name.

A plain row is a dotted PREFIX: it claims the package it names and every module beneath it. A row carrying a glob metacharacter (:data:GLOB_CHARS) is matched with :func:fnmatch.fnmatchcase instead, so it claims only the names the pattern itself spells out.

The glob form exists for one shape the prefix form cannot express: a module whose dotted name is a prefix of the tree it lives in. The root re-export shim symfonic is the only such module here โ€” a bare symfonic row would become the silent fallback owner of every future top-level package added under src/symfonic/, which would inherit a layer instead of raising layer-mapping and being ruled on its contents. symfoni[c] matches the top-level module and nothing below it, which is the same idiom the exception registry already uses for its targets.

Ties break on pattern length, longest wins, so a narrower row always overrides a broader one.

Source code in src/symfonic/devtools/archcheck/config.py
def layer_of(self, module: str) -> str | None:
    """Longest-pattern layer lookup for a dotted module name.

    A plain row is a dotted PREFIX: it claims the package it names and
    every module beneath it. A row carrying a glob metacharacter
    (:data:`GLOB_CHARS`) is matched with :func:`fnmatch.fnmatchcase`
    instead, so it claims only the names the pattern itself spells out.

    The glob form exists for one shape the prefix form cannot express: a
    module whose dotted name is a prefix of the tree it lives in. The root
    re-export shim ``symfonic`` is the only such module here โ€” a bare
    ``symfonic`` row would become the silent fallback owner of every future
    top-level package added under ``src/symfonic/``, which would inherit a
    layer instead of raising ``layer-mapping`` and being ruled on its
    contents. ``symfoni[c]`` matches the top-level module and nothing
    below it, which is the same idiom the exception registry already uses
    for its targets.

    Ties break on pattern length, longest wins, so a narrower row always
    overrides a broader one.
    """
    best: str | None = None
    best_len = -1
    for pattern, layer in self.layer_map.items():
        if _pattern_matches(pattern, module) and len(pattern) > best_len:
            best, best_len = layer, len(pattern)
    return best

ImportRecord dataclass

ImportRecord(target: str, lineno: int, lazy: bool = False, type_checking: bool = False)

One import statement found in a module.

ModuleInfo dataclass

ModuleInfo(name: str, path: Path, line_count: int, imports: tuple[ImportRecord, ...] = (), dunder_all: tuple[str, ...] | None = None, is_package: bool = False)

Static facts about one Python module in the scanned tree.

Violation dataclass

Violation(rule: str, message: str, modules: tuple[str, ...] = tuple(), edge: tuple[str, str] | None = None)

One architecture-rule violation.

modules lists every module implicated (one for most rules, the whole strongly connected component for cycles); the exception registry matches against this tuple.

edge is the (importer, imported) pair for the rules that are about one import statement. It exists because modules cannot express them: an import rule records modules=(importer,), so a waiver matched on modules alone waives every edge that importer will ever grow, not the one edge that was reviewed. The registry refuses such a waiver (registry-hygiene) and requires the edge instead.