Skip to content

cycle_ratio: split import-cycles (real smell) from intra-module call-cycles (benign); flat repos give false criticals #176

Description

@zaebee

Summary

The cycle_ratio hygiene metric conflates two structurally different things: cross-module import cycles (a real architectural smell) and intra-module call cycles (classes/functions in one file referencing each other — usually benign cohesion). On flat / one-file-per-concern repos this makes cycle_ratio {max: 0.0} fire on almost every domain, rendering drift useless as a CI gate.

Reproduction (two contrasting real repos)

A. Layered repo (package-per-domain) — cycles are REAL.
A FastAPI backend ingested as multi-file domains. cgis_drift flagged:

app.services         cycle_ratio 0.075 > 0   ← genuine cross-module circular import
app.api.dependencies cycle_ratio 0.016 > 0   ← genuine circular import in DI providers

These are true smells worth fixing.

B. Flat repo (one file per concern) — cycles are mostly ARTIFACT.
httpx (2106 nodes / 5848 edges, unresolved 9.5% → healthy), each httpx._<module> bound as a domain:

httpx._config      cycle_ratio 1.00   (exception/config classes referencing each other in ONE file)
httpx._exceptions  cycle_ratio 1.00
httpx._content     cycle_ratio 0.96
httpx._decoders    cycle_ratio 0.96
httpx._models      cycle_ratio 0.85   (Request/Response/Headers methods calling each other)
httpx._utils       cycle_ratio 0.74

httpx is a well-architected library — these are intra-file method/class references (cohesion), not circular module dependencies. Result: 8/10 domains went critical for a non-problem, drowning the one meaningful signal (httpx._client 0.41, no cycle).

Why it matters

cycle_ratio is currently a global hard gate. As-is it only behaves sensibly when domains map to multi-file packages. On flat layouts (or any single-module domain) it produces a wall of false criticals, so drift can't be trusted in CI for those repos.

Proposal

Split the metric by layer / scope:

  • import_cycle_ratio — cycles in the IMPORTS subgraph, restricted to cross-module/file edges. This is the real smell → keep as a hard hygiene gate.
  • call_cycle_ratio — cycles in the CALLS subgraph. Intra-module method recursion/mutual reference should be excluded or heavily discounted (optionally configurable: intra_module_calls: ignore|warn).

At minimum: exclude same-file (same-module) edges from the cycle computation, and/or report the two ratios separately so the hygiene gate can target import_cycle_ratio only.

Bonus: when a domain resolves to a single file/module, emit a notice (ties into init-ontology FR #174 — such domains probably shouldn't be cycle-gated at module granularity at all).

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions