Skip to content

symfonic.devtools.archgate

archgate

The permanent architecture and DRY gate (T4.4.1, REQ-S4.4).

One command CI runs, composing three checkers that already existed and one that did not:

  • architecture rules (T1.2.4) — dependency direction, cycles, module budgets, public-export drift, and the expiring exception registry.
  • optional imports (T4.2.1) — IA-1..IA-5 against the integration-adapter map.
  • DRY invariants (this task) — one invocation pipeline, one normalizer, and no forbidden process globals.
  • exception expiry (this task) — countdown, overdue review, and dead waivers over every registry the gate reads.

Run python -m symfonic.devtools.archgate from the repository root. Configuration: .agent/team/framework-refactor/evidence/T4.4.1/archgate.toml.

ArchgateConfigError

Bases: ValueError

archgate.toml is missing or structurally invalid.

SingleDefinitionRule dataclass

SingleDefinitionRule(id: str, rule: str, description: str, names: tuple[str, ...], owners: tuple[str, ...])

Names that may only be defined at top level by declared owners.

SingleSourceRule dataclass

SingleSourceRule(id: str, rule: str, description: str, targets: tuple[str, ...], owners: tuple[str, ...], allow_lazy: bool = False)

One chokepoint and the modules licensed to reach it.

targets are dotted import targets (a module prefix or a symbol). A module that is not an owner and imports one of them is a violation. allow_lazy exists for chokepoints whose deferred import is sanctioned; it is off by default, because a lazy import is still a second way in.

run_gate

run_gate(config: ArchgateConfig, today: date | None = None, strict_hygiene: bool = False) -> GateResult

Run every composed checker against the repository the config names.

Source code in src/symfonic/devtools/archgate/suite.py
def run_gate(
    config: ArchgateConfig,
    today: dt.date | None = None,
    strict_hygiene: bool = False,
) -> GateResult:
    """Run every composed checker against the repository the config names."""
    archcheck_config = load_config(config.archcheck_config_path)
    archcheck_result = run_all(archcheck_config, today=today)
    modules = archcheck_result.modules or scan_source_tree(config.src_root)

    integration_map = load_map(config.integration_map_path)
    integrations = tuple(integration_violations(modules, integration_map))

    facts = collect_facts(modules)
    raw_dry = check_single_source(modules, config.single_source_rules)
    raw_dry += check_single_definition(facts, config.single_definition_rules)
    raw_dry += check_forbidden_globals(facts, config.sanctioned_global_modules)

    dry_entries = load_registry(config.registry_path)
    active, suppressed = apply_exceptions(raw_dry, dry_entries, today=today)
    active.extend(expired_entries(dry_entries, today=today))
    active.sort(key=lambda violation: (violation.rule, violation.modules))

    coverage, coverage_suppressed = apply_exceptions(
        check_export_coverage(modules, archcheck_config), dry_entries, today=today
    )
    suppressed.extend(coverage_suppressed)

    # RCH-1: a capability/service package no composition root imports.
    # Registry-suppressible like every other rule, so the eleven the refactor
    # left unreached are debt with an expiry rather than an invisible default.
    reachability, reachability_suppressed = apply_exceptions(
        check_reachability(modules), dry_entries, today=today
    )
    suppressed.extend(reachability_suppressed)

    all_entries = (*archcheck_result.exceptions, *dry_entries)
    all_suppressed = (*archcheck_result.suppressed, *suppressed)
    expiry = build_expiry_report(
        all_entries,
        all_suppressed,
        today=today,
        horizon_days=config.expiry_horizon_days,
    )

    rule_ids = tuple(
        rule.id
        for rule in (*config.single_source_rules, *config.single_definition_rules)
    ) + ("GLB-1",)

    return GateResult(
        config=config,
        archcheck=archcheck_result,
        integration_violations=integrations,
        integration_adapters=integration_map.entries,
        dry_violations=tuple(active),
        export_coverage_violations=tuple(coverage),
        reachability_violations=tuple(reachability),
        dry_suppressed=tuple(suppressed),
        dry_exceptions=dry_entries,
        dry_rule_ids=rule_ids,
        expiry=expiry,
        hygiene_violations=tuple(check_registry_hygiene(all_entries)),
        strict_hygiene=strict_hygiene,
    )