Skip to content

symfonic.devtools.archgate.inventory

inventory

Machine-readable inventory of modules outside the configured layer map.

BaselineMismatchError

Bases: ValueError

The live measurement disagrees with the independently pinned baseline.

InventoryEnvironmentError

Bases: RuntimeError

The repository cannot provide the commit needed for comparison.

build_inventory

build_inventory(config: ArchgateConfig, result: GateResult) -> Inventory

Measure the live gate result and fail if any baseline pin moved.

Source code in src/symfonic/devtools/archgate/inventory.py
def build_inventory(config: ArchgateConfig, result: GateResult) -> Inventory:
    """Measure the live gate result and fail if any baseline pin moved."""
    arch_config = load_config(config.archcheck_config_path)
    suppressed = (*result.archcheck.suppressed, *result.dry_suppressed)
    layer_suppressed = [item for item in suppressed if item[0].rule == "layer-mapping"]
    exception_counts = {
        entry_id: sum(1 for _, matched_id in suppressed if matched_id == entry_id)
        for entry_id in ("EXC-2026-001", "EXC-2026-004")
    }
    legacy_entry = entry(result, "EXC-2026-001")
    package_counts = {
        target: sum(
            1
            for violation, entry_id in layer_suppressed
            if entry_id == legacy_entry.id and plain_target_matches(target, violation.modules)
        )
        for target in legacy_entry.targets
    }
    live_modules = set(result.archcheck.modules)
    baseline_modules = _modules_at_commit(config.repo_root, PINNED_BASELINE.measured_commit)
    excluded_modules = sorted(live_modules - baseline_modules)
    removed_modules = sorted(baseline_modules - live_modules)
    expected_excluded_modules = [
        "symfonic.devtools.archgate.baseline",
        "symfonic.devtools.archgate.inventory",
        "symfonic.devtools.archgate.ratchet",
    ]
    if excluded_modules != expected_excluded_modules:
        raise BaselineMismatchError(
            f"post-baseline modules {excluded_modules!r} disagree with expected "
            f"additions {expected_excluded_modules!r}"
        )
    modules_scanned_at_baseline_intersection = len(live_modules & baseline_modules)
    totals: dict[str, object] = {
        "all_rule_total": len(suppressed),
        "layer_mapping_total": len(layer_suppressed),
        "modules_scanned_at_baseline_intersection": (
            modules_scanned_at_baseline_intersection
        ),
        "per_exception": exception_counts,
    }
    if totals != PINNED_BASELINE.as_totals():
        raise BaselineMismatchError(
            f"live suppression counts {totals!r} disagree with pins "
            f"{PINNED_BASELINE.as_totals()!r}"
        )
    if package_counts != PINNED_BASELINE.exc_2026_001_per_package:
        raise BaselineMismatchError(
            f"live EXC-2026-001 package counts {package_counts!r} disagree with pins "
            f"{PINNED_BASELINE.exc_2026_001_per_package!r}"
        )
    package_sum = sum(package_counts.values())
    if package_sum != PINNED_BASELINE.exc_2026_001:
        raise BaselineMismatchError(
            f"EXC-2026-001 package sum {package_sum} disagrees with pin "
            f"{PINNED_BASELINE.exc_2026_001}"
        )

    content_digests = {
        "archcheck.toml": _digest(config.archcheck_config_path),
        "dry-exception-registry.toml": _digest(config.registry_path),
        "exception-registry.toml": _digest(arch_config.registry_path),
    }
    if content_digests != PINNED_BASELINE.content_digests:
        raise BaselineMismatchError(
            f"live input digests {content_digests!r} disagree with pins "
            f"{PINNED_BASELINE.content_digests!r}"
        )

    return {
        "schema_version": 2,
        "baseline_date": "2026-08-21",
        "baseline_measured_commit": PINNED_BASELINE.measured_commit,
        "content_digests": content_digests,
        "totals": totals,
        "modules_scanned_live": len(live_modules),
        "modules_excluded_post_baseline": excluded_modules,
        "modules_removed_since_baseline": removed_modules,
        "modules_scanned_at_baseline": len(baseline_modules),
        "exc_2026_001_per_package": {
            "counts": dict(sorted(package_counts.items())),
            "sum": package_sum,
        },
        "unmapped_modules": sorted(
            module
            for module in result.archcheck.modules
            if arch_config.layer_of(module) is None
        ),
    }

write_inventory

write_inventory(path: Path, inventory: Inventory) -> None

Write deterministic, diff-friendly JSON with a trailing newline.

Source code in src/symfonic/devtools/archgate/inventory.py
def write_inventory(path: Path, inventory: Inventory) -> None:
    """Write deterministic, diff-friendly JSON with a trailing newline."""
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(
        json.dumps(inventory, indent=2, sort_keys=True) + "\n",
        encoding="utf-8",
    )