Node god-class and coupling rules — backed by axm-ast structural metrics.
Unlike the other node rules (which shell out to ESLint/knip/madge), these read
structural facts that axm-ast already computes from the TS AST and dependency
graph: class size/method-count and per-module fan-out. axm-ast is the source of
truth for the fact; this rule only applies the AXM threshold — the same split as
the Python GodClassRule / CouplingMetricRule (which compute on the Python
AST). One axm-ast implementation thus powers both languages.
NodeCouplingRule
Bases: ProjectRule
Flag over-coupled modules (high fan-out) via axm-ast.
Mirrors the Python CouplingMetricRule: modules with fan-out > 10,
100 - count * 5.
Source code in packages/axm-audit/src/axm_audit/core/rules/node/coupling.py
| Python |
|---|
| @register_rule("architecture", framework=Framework.NODE)
class NodeCouplingRule(ProjectRule):
"""Flag over-coupled modules (high fan-out) via axm-ast.
Mirrors the Python ``CouplingMetricRule``: modules with fan-out > 10,
``100 - count * 5``.
"""
@property
def rule_id(self) -> str:
"""Unique identifier (shared with the Python coupling rule)."""
return "ARCH_COUPLING"
def check(self, project_path: Path) -> CheckResult:
"""Score by the count of modules whose fan-out exceeds the threshold."""
if not (project_path / "package.json").is_file():
return _skip(self.rule_id)
pkg = _analyze(project_path)
if pkg is None:
return _unavailable(self.rule_id, "axm-ast")
from axm_ast.core.metrics import compute_coupling
metrics = compute_coupling(pkg)
over = metrics.over_fan_out(_FAN_OUT_THRESHOLD)
score = max(0, 100 - len(over) * _COUPLING_PENALTY)
passed = not over
return CheckResult(
rule_id=self.rule_id,
passed=passed,
message=(
f"{len(over)} over-coupled module(s) "
f"(max fan-out {metrics.max_fan_out})"
),
severity=Severity.WARNING if not passed else Severity.INFO,
score=score,
details={
"max_fan_out": metrics.max_fan_out,
"over_threshold": [f"{m.module} fo:{m.fan_out}" for m in over[:20]],
},
fix_hint="Reduce imports in the listed modules" if over else None,
)
|
rule_id
property
Unique identifier (shared with the Python coupling rule).
check(project_path)
Score by the count of modules whose fan-out exceeds the threshold.
Source code in packages/axm-audit/src/axm_audit/core/rules/node/coupling.py
| Python |
|---|
| def check(self, project_path: Path) -> CheckResult:
"""Score by the count of modules whose fan-out exceeds the threshold."""
if not (project_path / "package.json").is_file():
return _skip(self.rule_id)
pkg = _analyze(project_path)
if pkg is None:
return _unavailable(self.rule_id, "axm-ast")
from axm_ast.core.metrics import compute_coupling
metrics = compute_coupling(pkg)
over = metrics.over_fan_out(_FAN_OUT_THRESHOLD)
score = max(0, 100 - len(over) * _COUPLING_PENALTY)
passed = not over
return CheckResult(
rule_id=self.rule_id,
passed=passed,
message=(
f"{len(over)} over-coupled module(s) "
f"(max fan-out {metrics.max_fan_out})"
),
severity=Severity.WARNING if not passed else Severity.INFO,
score=score,
details={
"max_fan_out": metrics.max_fan_out,
"over_threshold": [f"{m.module} fo:{m.fan_out}" for m in over[:20]],
},
fix_hint="Reduce imports in the listed modules" if over else None,
)
|
NodeGodClassRule
Bases: ProjectRule
Flag god classes (too many lines or methods) via axm-ast.
Mirrors the Python GodClassRule: lines > 500 or methods > 15,
100 - count * 15.
Source code in packages/axm-audit/src/axm_audit/core/rules/node/coupling.py
| Python |
|---|
| @register_rule("architecture", framework=Framework.NODE)
class NodeGodClassRule(ProjectRule):
"""Flag god classes (too many lines or methods) via axm-ast.
Mirrors the Python ``GodClassRule``: lines > 500 or methods > 15,
``100 - count * 15``.
"""
@property
def rule_id(self) -> str:
"""Unique identifier (shared with the Python god-class rule)."""
return "ARCH_GOD_CLASS"
def check(self, project_path: Path) -> CheckResult:
"""Score by the count of god classes found in the package's TS classes."""
if not (project_path / "package.json").is_file():
return _skip(self.rule_id)
pkg = _analyze(project_path)
if pkg is None:
return _unavailable(self.rule_id, "axm-ast")
from axm_ast.core.metrics import find_god_classes
god = find_god_classes(pkg)
score = max(0, 100 - len(god) * _GOD_CLASS_PENALTY)
passed = not god
return CheckResult(
rule_id=self.rule_id,
passed=passed,
message=f"{len(god)} god class(es) found",
severity=Severity.WARNING if not passed else Severity.INFO,
score=score,
details={
"god_classes": [
f"{g.file}:{g.name} {g.lines}L/{g.methods}M" for g in god[:20]
]
},
fix_hint="Split large classes into smaller, focused ones" if god else None,
)
|
rule_id
property
Unique identifier (shared with the Python god-class rule).
check(project_path)
Score by the count of god classes found in the package's TS classes.
Source code in packages/axm-audit/src/axm_audit/core/rules/node/coupling.py
| Python |
|---|
| def check(self, project_path: Path) -> CheckResult:
"""Score by the count of god classes found in the package's TS classes."""
if not (project_path / "package.json").is_file():
return _skip(self.rule_id)
pkg = _analyze(project_path)
if pkg is None:
return _unavailable(self.rule_id, "axm-ast")
from axm_ast.core.metrics import find_god_classes
god = find_god_classes(pkg)
score = max(0, 100 - len(god) * _GOD_CLASS_PENALTY)
passed = not god
return CheckResult(
rule_id=self.rule_id,
passed=passed,
message=f"{len(god)} god class(es) found",
severity=Severity.WARNING if not passed else Severity.INFO,
score=score,
details={
"god_classes": [
f"{g.file}:{g.name} {g.lines}L/{g.methods}M" for g in god[:20]
]
},
fix_hint="Split large classes into smaller, focused ones" if god else None,
)
|