Skip to content

Index

node

Node/Svelte/React rule implementations (the shared node base layer).

These rules port the intent of the Python rules to the Node ecosystem (ESLint, tsc, prettier, vitest, npm audit, knip, madge, jscpd, gitleaks …) without changing axm-audit's scoring, categories, or CheckResult contract. They register under the node framework via @register_rule(category, framework=NODE); UI frameworks (svelte, react) inherit them via resolve_frameworks.

Importing this package fires the @register_rule decorators (side effect).

NodeCircularImportRule

Bases: NodeToolRule

Score circular-import cycles found by madge --circular --json.

Mirrors the Python CircularImportRule: 100 - cycles * 20. madge --circular --json returns a JSON array of cycles (each a list of files); an empty array means no cycles.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/architecture.py
Python
@register_rule("architecture", framework=Framework.NODE)
class NodeCircularImportRule(NodeToolRule):
    """Score circular-import cycles found by ``madge --circular --json``.

    Mirrors the Python ``CircularImportRule``: ``100 - cycles * 20``.
    ``madge --circular --json`` returns a JSON array of cycles (each a list of
    files); an empty array means no cycles.
    """

    binary = "madge"
    install_hint = "Install madge: npm install -D madge"

    @property
    def rule_id(self) -> str:
        """Unique identifier (shared with the Python circular-import rule)."""
        return "ARCH_CIRCULAR"

    @property
    def args(self) -> list[str]:
        """Report circular dependencies as JSON over the source tree."""
        return ["--circular", "--json", "src"]

    def score_output(self, parsed: object, project_path: Path) -> CheckResult:
        """Score by the number of import cycles."""
        cycle_count = len(parsed) if isinstance(parsed, list) else 0
        score = max(0, 100 - cycle_count * 20)
        passed = score >= PASS_THRESHOLD
        return CheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=f"Circular imports: {cycle_count} cycle(s)",
            severity=Severity.ERROR if not passed else Severity.INFO,
            score=score,
            details={"cycle_count": cycle_count},
            fix_hint="Break the import cycles above" if cycle_count else None,
        )
args property

Report circular dependencies as JSON over the source tree.

rule_id property

Unique identifier (shared with the Python circular-import rule).

score_output(parsed, project_path)

Score by the number of import cycles.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/architecture.py
Python
def score_output(self, parsed: object, project_path: Path) -> CheckResult:
    """Score by the number of import cycles."""
    cycle_count = len(parsed) if isinstance(parsed, list) else 0
    score = max(0, 100 - cycle_count * 20)
    passed = score >= PASS_THRESHOLD
    return CheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=f"Circular imports: {cycle_count} cycle(s)",
        severity=Severity.ERROR if not passed else Severity.INFO,
        score=score,
        details={"cycle_count": cycle_count},
        fix_hint="Break the import cycles above" if cycle_count else None,
    )

NodeComplexityRule

Bases: NodeToolRule

Score cyclomatic + cognitive complexity findings from ESLint.

Scoring: 100 - violations * 10, min 0 — identical to the Python rule.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/complexity.py
Python
@register_rule("complexity", framework=Framework.NODE)
class NodeComplexityRule(NodeToolRule):
    """Score cyclomatic + cognitive complexity findings from ESLint.

    Scoring: ``100 - violations * 10``, min 0 — identical to the Python rule.
    """

    binary = "eslint"
    install_hint = (
        "Install ESLint + sonarjs: npm install -D eslint eslint-plugin-sonarjs"
    )

    @property
    def rule_id(self) -> str:
        """Unique identifier (shared with the Python complexity rule)."""
        return "QUALITY_COMPLEXITY"

    @property
    def args(self) -> list[str]:
        """Run ESLint over the project with the JSON formatter."""
        return ["--format", "json", "."]

    def score_output(self, parsed: object, project_path: Path) -> CheckResult:
        """Score by the count of complexity-rule violations."""
        count = _count_complexity_messages(parsed) if isinstance(parsed, list) else 0
        score = max(0, 100 - count * _PENALTY)
        passed = score >= PASS_THRESHOLD
        return CheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=f"Complexity score: {score}/100 ({count} over threshold)",
            severity=Severity.WARNING if not passed else Severity.INFO,
            score=score,
            details={"violation_count": count},
            fix_hint=("Refactor functions above cc<10 / cog<15" if count > 0 else None),
        )
args property

Run ESLint over the project with the JSON formatter.

rule_id property

Unique identifier (shared with the Python complexity rule).

score_output(parsed, project_path)

Score by the count of complexity-rule violations.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/complexity.py
Python
def score_output(self, parsed: object, project_path: Path) -> CheckResult:
    """Score by the count of complexity-rule violations."""
    count = _count_complexity_messages(parsed) if isinstance(parsed, list) else 0
    score = max(0, 100 - count * _PENALTY)
    passed = score >= PASS_THRESHOLD
    return CheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=f"Complexity score: {score}/100 ({count} over threshold)",
        severity=Severity.WARNING if not passed else Severity.INFO,
        score=score,
        details={"violation_count": count},
        fix_hint=("Refactor functions above cc<10 / cog<15" if count > 0 else None),
    )

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,
    )

NodeDeadCodeRule

Bases: _KnipRule

Score unused files + unused exports (knip).

Mirrors the Python DeadCodeRule (category lint): 100 - items*10.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/knip.py
Python
@register_rule("lint", framework=Framework.NODE)
class NodeDeadCodeRule(_KnipRule):
    """Score unused files + unused exports (knip).

    Mirrors the Python ``DeadCodeRule`` (category ``lint``): ``100 - items*10``.
    """

    @property
    def rule_id(self) -> str:
        """Unique identifier (shared with the Python dead-code rule)."""
        return "QUALITY_DEAD_CODE"

    def score_output(self, parsed: object, project_path: Path) -> CheckResult:
        """Score by the count of unused files + unused exports/types."""
        count = _count_unused_files(parsed) + _sum_issue_kinds(parsed, _DEAD_KINDS)
        score = max(0, 100 - count * 10)
        passed = score >= PASS_THRESHOLD
        return CheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=f"Dead code: {score}/100 ({count} unused)",
            severity=Severity.WARNING if not passed else Severity.INFO,
            score=score,
            details={"unused_count": count},
            fix_hint="Remove unused files / exports" if count else None,
        )
rule_id property

Unique identifier (shared with the Python dead-code rule).

score_output(parsed, project_path)

Score by the count of unused files + unused exports/types.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/knip.py
Python
def score_output(self, parsed: object, project_path: Path) -> CheckResult:
    """Score by the count of unused files + unused exports/types."""
    count = _count_unused_files(parsed) + _sum_issue_kinds(parsed, _DEAD_KINDS)
    score = max(0, 100 - count * 10)
    passed = score >= PASS_THRESHOLD
    return CheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=f"Dead code: {score}/100 ({count} unused)",
        severity=Severity.WARNING if not passed else Severity.INFO,
        score=score,
        details={"unused_count": count},
        fix_hint="Remove unused files / exports" if count else None,
    )

NodeDependencyRule

Bases: _KnipRule

Score unused + unlisted dependency hygiene (knip).

Mirrors the Python DependencyHygieneRule: 100 - issues * 10.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/knip.py
Python
@register_rule("deps", framework=Framework.NODE)
class NodeDependencyRule(_KnipRule):
    """Score unused + unlisted dependency hygiene (knip).

    Mirrors the Python ``DependencyHygieneRule``: ``100 - issues * 10``.
    """

    @property
    def rule_id(self) -> str:
        """Unique identifier for this rule."""
        return "DEPS_HYGIENE"

    def score_output(self, parsed: object, project_path: Path) -> CheckResult:
        """Score by the count of unused/unlisted dependency issues."""
        count = _sum_issue_kinds(parsed, _DEP_KINDS)
        score = max(0, 100 - count * 10)
        passed = score >= PASS_THRESHOLD
        return CheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=f"Dependency hygiene: {score}/100 ({count} issues)",
            severity=Severity.WARNING if not passed else Severity.INFO,
            score=score,
            details={"issue_count": count},
            fix_hint="Remove unused / declare unlisted deps" if count else None,
        )
rule_id property

Unique identifier for this rule.

score_output(parsed, project_path)

Score by the count of unused/unlisted dependency issues.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/knip.py
Python
def score_output(self, parsed: object, project_path: Path) -> CheckResult:
    """Score by the count of unused/unlisted dependency issues."""
    count = _sum_issue_kinds(parsed, _DEP_KINDS)
    score = max(0, 100 - count * 10)
    passed = score >= PASS_THRESHOLD
    return CheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=f"Dependency hygiene: {score}/100 ({count} issues)",
        severity=Severity.WARNING if not passed else Severity.INFO,
        score=score,
        details={"issue_count": count},
        fix_hint="Remove unused / declare unlisted deps" if count else None,
    )

NodeDiffSizeRule dataclass

Bases: DiffSizeRule

Diff-size rule for node projects — identical git-based logic.

git diff is language-agnostic, so this reuses the Python implementation wholesale; only the framework registration differs. Lives in lint like the Python DiffSizeRule.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/quality.py
Python
@register_rule("lint", framework=Framework.NODE)
class NodeDiffSizeRule(DiffSizeRule):
    """Diff-size rule for node projects — identical git-based logic.

    ``git diff`` is language-agnostic, so this reuses the Python
    implementation wholesale; only the framework registration differs.
    Lives in ``lint`` like the Python ``DiffSizeRule``.
    """

NodeDuplicationRule

Bases: NodeToolRule

Score code duplication found by jscpd --reporters json.

Mirrors the Python DuplicationRule intent. jscpd reports a duplicated percentage; we map it to a score (0% → 100, ≥10% → 0) and pass below a 3% duplication threshold (the research's recommended ceiling).

Source code in packages/axm-audit/src/axm_audit/core/rules/node/architecture.py
Python
@register_rule("architecture", framework=Framework.NODE)
class NodeDuplicationRule(NodeToolRule):
    """Score code duplication found by ``jscpd --reporters json``.

    Mirrors the Python ``DuplicationRule`` intent. jscpd reports a duplicated
    percentage; we map it to a score (0% → 100, ≥10% → 0) and pass below a 3%
    duplication threshold (the research's recommended ceiling).
    """

    binary = "jscpd"
    install_hint = "Install jscpd: npm install -D jscpd"
    _MAX_TOLERATED_PCT = 10.0
    _PASS_PCT = 3.0

    @property
    def rule_id(self) -> str:
        """Unique identifier (shared with the Python duplication rule)."""
        return "ARCH_DUPLICATION"

    @property
    def args(self) -> list[str]:
        """Placeholder — :meth:`check` builds the argv with a real output dir."""
        return ["--reporters", "json", "--silent", "src"]

    def check(self, project_path: Path) -> CheckResult:
        """Run jscpd, reading the percentage from its JSON *report file*.

        jscpd's ``json`` reporter writes ``jscpd-report.json`` to an output
        directory; **nothing structured lands on stdout** (only a one-line human
        summary). The base :class:`NodeToolRule.parse` JSON-decodes stdout, which
        for jscpd always yields ``[]`` → pct 0 → a permanent false-green. So this
        rule reads the report file instead of stdout.
        """
        if not (project_path / "package.json").is_file():
            return CheckResult(
                rule_id=self.rule_id,
                passed=True,
                message="No package.json — jscpd skipped",
                severity=Severity.INFO,
                score=100,
            )
        if not node_tool_available(project_path, self.binary):
            return CheckResult(
                rule_id=self.rule_id,
                passed=False,
                message=f"{self.binary} not available "
                f"(not on node_modules/.bin/{self.binary})",
                severity=Severity.ERROR,
                fix_hint=self.install_hint,
            )
        with tempfile.TemporaryDirectory() as report_dir:
            result = run_node_tool(
                self.binary,
                ["--reporters", "json", "--output", report_dir, "--silent", "src"],
                project_path,
                on_path=False,
            )
            if interpret_process(result) is ProcessVerdict.ENV_FAILURE:
                return self.env_failure_result(result.returncode)
            report = Path(report_dir) / "jscpd-report.json"
            try:
                parsed = json.loads(report.read_text(encoding="utf-8"))
            except (OSError, json.JSONDecodeError):
                parsed = {}
        return self.score_output(parsed, project_path)

    def score_output(self, parsed: object, project_path: Path) -> CheckResult:
        """Score inversely to the duplicated-token percentage."""
        pct = _jscpd_percentage(parsed)
        score = max(0, round(100 - (pct / self._MAX_TOLERATED_PCT) * 100))
        passed = pct <= self._PASS_PCT
        return CheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=f"Duplication: {pct:.1f}% ({score}/100)",
            severity=Severity.WARNING if not passed else Severity.INFO,
            score=score,
            details={"duplication_pct": pct},
            fix_hint="Reduce duplicated code blocks" if not passed else None,
        )
args property

Placeholder — :meth:check builds the argv with a real output dir.

rule_id property

Unique identifier (shared with the Python duplication rule).

check(project_path)

Run jscpd, reading the percentage from its JSON report file.

jscpd's json reporter writes jscpd-report.json to an output directory; nothing structured lands on stdout (only a one-line human summary). The base :class:NodeToolRule.parse JSON-decodes stdout, which for jscpd always yields [] → pct 0 → a permanent false-green. So this rule reads the report file instead of stdout.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/architecture.py
Python
def check(self, project_path: Path) -> CheckResult:
    """Run jscpd, reading the percentage from its JSON *report file*.

    jscpd's ``json`` reporter writes ``jscpd-report.json`` to an output
    directory; **nothing structured lands on stdout** (only a one-line human
    summary). The base :class:`NodeToolRule.parse` JSON-decodes stdout, which
    for jscpd always yields ``[]`` → pct 0 → a permanent false-green. So this
    rule reads the report file instead of stdout.
    """
    if not (project_path / "package.json").is_file():
        return CheckResult(
            rule_id=self.rule_id,
            passed=True,
            message="No package.json — jscpd skipped",
            severity=Severity.INFO,
            score=100,
        )
    if not node_tool_available(project_path, self.binary):
        return CheckResult(
            rule_id=self.rule_id,
            passed=False,
            message=f"{self.binary} not available "
            f"(not on node_modules/.bin/{self.binary})",
            severity=Severity.ERROR,
            fix_hint=self.install_hint,
        )
    with tempfile.TemporaryDirectory() as report_dir:
        result = run_node_tool(
            self.binary,
            ["--reporters", "json", "--output", report_dir, "--silent", "src"],
            project_path,
            on_path=False,
        )
        if interpret_process(result) is ProcessVerdict.ENV_FAILURE:
            return self.env_failure_result(result.returncode)
        report = Path(report_dir) / "jscpd-report.json"
        try:
            parsed = json.loads(report.read_text(encoding="utf-8"))
        except (OSError, json.JSONDecodeError):
            parsed = {}
    return self.score_output(parsed, project_path)
score_output(parsed, project_path)

Score inversely to the duplicated-token percentage.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/architecture.py
Python
def score_output(self, parsed: object, project_path: Path) -> CheckResult:
    """Score inversely to the duplicated-token percentage."""
    pct = _jscpd_percentage(parsed)
    score = max(0, round(100 - (pct / self._MAX_TOLERATED_PCT) * 100))
    passed = pct <= self._PASS_PCT
    return CheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=f"Duplication: {pct:.1f}% ({score}/100)",
        severity=Severity.WARNING if not passed else Severity.INFO,
        score=score,
        details={"duplication_pct": pct},
        fix_hint="Reduce duplicated code blocks" if not passed else None,
    )

NodeFormatRule

Bases: NodeToolRule

Run prettier --check and score by the unformatted-file count.

Lives in the lint category to mirror the Python FormattingRule. Scoring: 100 - files * 5, min 0.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/format.py
Python
@register_rule("lint", framework=Framework.NODE)
class NodeFormatRule(NodeToolRule):
    """Run ``prettier --check`` and score by the unformatted-file count.

    Lives in the ``lint`` category to mirror the Python ``FormattingRule``.
    Scoring: ``100 - files * 5``, min 0.
    """

    binary = "prettier"
    install_hint = "Install Prettier: npm install -D prettier"

    @property
    def rule_id(self) -> str:
        """Unique identifier for this rule (shared with the Python format rule)."""
        return "QUALITY_FORMAT"

    @property
    def args(self) -> list[str]:
        """Check formatting across the project without writing changes."""
        return ["--check", "."]

    @property
    def findings_returncodes(self) -> frozenset[int]:
        """Prettier exits 1 when files are unformatted — that is a finding."""
        return frozenset({1})

    def parse(self, result: subprocess.CompletedProcess[str]) -> object:
        """Prettier reports unformatted files on stderr — combine both streams."""
        return f"{result.stdout}\n{result.stderr}"

    def score_output(self, parsed: object, project_path: Path) -> CheckResult:
        """Score by the count of unformatted files (parsed is combined output)."""
        output = parsed if isinstance(parsed, str) else ""
        file_count = _count_unformatted(output)
        score = max(0, 100 - file_count * 5)
        passed = score >= PASS_THRESHOLD
        return CheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=f"Format score: {score}/100 ({file_count} unformatted)",
            severity=Severity.WARNING if not passed else Severity.INFO,
            score=score,
            details={"unformatted_count": file_count},
            fix_hint="Run: npx prettier --write ." if file_count > 0 else None,
        )
args property

Check formatting across the project without writing changes.

findings_returncodes property

Prettier exits 1 when files are unformatted — that is a finding.

rule_id property

Unique identifier for this rule (shared with the Python format rule).

parse(result)

Prettier reports unformatted files on stderr — combine both streams.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/format.py
Python
def parse(self, result: subprocess.CompletedProcess[str]) -> object:
    """Prettier reports unformatted files on stderr — combine both streams."""
    return f"{result.stdout}\n{result.stderr}"
score_output(parsed, project_path)

Score by the count of unformatted files (parsed is combined output).

Source code in packages/axm-audit/src/axm_audit/core/rules/node/format.py
Python
def score_output(self, parsed: object, project_path: Path) -> CheckResult:
    """Score by the count of unformatted files (parsed is combined output)."""
    output = parsed if isinstance(parsed, str) else ""
    file_count = _count_unformatted(output)
    score = max(0, 100 - file_count * 5)
    passed = score >= PASS_THRESHOLD
    return CheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=f"Format score: {score}/100 ({file_count} unformatted)",
        severity=Severity.WARNING if not passed else Severity.INFO,
        score=score,
        details={"unformatted_count": file_count},
        fix_hint="Run: npx prettier --write ." if file_count > 0 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,
    )

NodeLintRule

Bases: NodeToolRule

Run ESLint and score based on issue count (Node/Svelte/React projects).

Scoring: 100 - issue_count * 2, min 0 — identical to the Python lint rule so the lint category is framework-agnostic at the scoring layer.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/lint.py
Python
@register_rule("lint", framework=Framework.NODE)
class NodeLintRule(NodeToolRule):
    """Run ESLint and score based on issue count (Node/Svelte/React projects).

    Scoring: ``100 - issue_count * 2``, min 0 — identical to the Python lint
    rule so the ``lint`` category is framework-agnostic at the scoring layer.
    """

    binary = "eslint"
    install_hint = "Install ESLint: npm install -D eslint"

    @property
    def rule_id(self) -> str:
        """Unique identifier for this rule (shared with the Python lint rule)."""
        return "QUALITY_LINT"

    @property
    def args(self) -> list[str]:
        """Run ESLint over the whole project with the JSON formatter."""
        return ["--format", "json", "."]

    def score_output(self, parsed: object, project_path: Path) -> CheckResult:
        """Score by the total ESLint error + warning count."""
        issue_count = _count_messages(parsed) if isinstance(parsed, list) else 0
        score = max(0, 100 - issue_count * 2)
        passed = score >= LINT_PASS_THRESHOLD
        return CheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=f"Lint score: {score}/100 ({issue_count} issues)",
            severity=Severity.WARNING if not passed else Severity.INFO,
            score=int(score),
            details={"issue_count": issue_count},
            fix_hint="Run: npx eslint --fix ." if issue_count > 0 else None,
        )
args property

Run ESLint over the whole project with the JSON formatter.

rule_id property

Unique identifier for this rule (shared with the Python lint rule).

score_output(parsed, project_path)

Score by the total ESLint error + warning count.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/lint.py
Python
def score_output(self, parsed: object, project_path: Path) -> CheckResult:
    """Score by the total ESLint error + warning count."""
    issue_count = _count_messages(parsed) if isinstance(parsed, list) else 0
    score = max(0, 100 - issue_count * 2)
    passed = score >= LINT_PASS_THRESHOLD
    return CheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=f"Lint score: {score}/100 ({issue_count} issues)",
        severity=Severity.WARNING if not passed else Severity.INFO,
        score=int(score),
        details={"issue_count": issue_count},
        fix_hint="Run: npx eslint --fix ." if issue_count > 0 else None,
    )

NodeSecretsRule

Bases: NodeToolRule

Score hardcoded-secret findings from gitleaks.

Mirrors the Python PRACTICE_SECURITY (secret scan): 100 - secrets*25. gitleaks is a system tool (not a node_modules binary); it writes its JSON report to stdout and exits non-zero when leaks are found.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/security.py
Python
@register_rule("security", framework=Framework.NODE)
class NodeSecretsRule(NodeToolRule):
    """Score hardcoded-secret findings from gitleaks.

    Mirrors the Python ``PRACTICE_SECURITY`` (secret scan): ``100 - secrets*25``.
    gitleaks is a system tool (not a node_modules binary); it writes its JSON
    report to stdout and exits non-zero when leaks are found.
    """

    binary = "gitleaks"
    on_path = True
    install_hint = "Install gitleaks: brew install gitleaks"

    @property
    def rule_id(self) -> str:
        """Unique identifier (shared with the Python secret-scan rule)."""
        return "PRACTICE_SECURITY"

    @property
    def args(self) -> list[str]:
        """Scan the directory, emitting the JSON report to stdout."""
        return ["dir", ".", "--report-format", "json", "--report-path", "/dev/stdout"]

    @property
    def findings_returncodes(self) -> frozenset[int]:
        """gitleaks exits 1 when leaks are found — a finding, not a crash."""
        return frozenset({1})

    def score_output(self, parsed: object, project_path: Path) -> CheckResult:
        """Score by the number of secret findings (gitleaks JSON is an array)."""
        secret_count = len(parsed) if isinstance(parsed, list) else 0
        score = max(0, 100 - secret_count * 25)
        passed = secret_count == 0
        return CheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=f"Secrets: {secret_count} hardcoded secret(s) found",
            severity=Severity.ERROR if not passed else Severity.INFO,
            score=score,
            details={"secret_count": secret_count},
            fix_hint="Remove/rotate the leaked secrets above" if secret_count else None,
        )
args property

Scan the directory, emitting the JSON report to stdout.

findings_returncodes property

gitleaks exits 1 when leaks are found — a finding, not a crash.

rule_id property

Unique identifier (shared with the Python secret-scan rule).

score_output(parsed, project_path)

Score by the number of secret findings (gitleaks JSON is an array).

Source code in packages/axm-audit/src/axm_audit/core/rules/node/security.py
Python
def score_output(self, parsed: object, project_path: Path) -> CheckResult:
    """Score by the number of secret findings (gitleaks JSON is an array)."""
    secret_count = len(parsed) if isinstance(parsed, list) else 0
    score = max(0, 100 - secret_count * 25)
    passed = secret_count == 0
    return CheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=f"Secrets: {secret_count} hardcoded secret(s) found",
        severity=Severity.ERROR if not passed else Severity.INFO,
        score=score,
        details={"secret_count": secret_count},
        fix_hint="Remove/rotate the leaked secrets above" if secret_count else None,
    )

NodeSecurityLintRule

Bases: NodeToolRule

Score eslint-plugin-security findings (the bandit pendant for TS/JS).

Mirrors the Python QUALITY_SECURITY: 100 - findings * 15. Reuses the project's ESLint config (which must enable eslint-plugin-security) and filters the security/* ruleIds from the JSON output.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/quality.py
Python
@register_rule("security", framework=Framework.NODE)
class NodeSecurityLintRule(NodeToolRule):
    """Score eslint-plugin-security findings (the bandit pendant for TS/JS).

    Mirrors the Python ``QUALITY_SECURITY``: ``100 - findings * 15``. Reuses the
    project's ESLint config (which must enable eslint-plugin-security) and
    filters the ``security/*`` ruleIds from the JSON output.
    """

    binary = "eslint"
    install_hint = (
        "Install eslint-plugin-security: npm install -D eslint-plugin-security"
    )

    @property
    def rule_id(self) -> str:
        """Unique identifier (shared with the Python security rule)."""
        return "QUALITY_SECURITY"

    @property
    def args(self) -> list[str]:
        """Run ESLint over the project with the JSON formatter."""
        return ["--format", "json", "."]

    def score_output(self, parsed: object, project_path: Path) -> CheckResult:
        """Score by the count of eslint-plugin-security findings."""
        count = _count_security_messages(parsed) if isinstance(parsed, list) else 0
        score = max(0, 100 - count * 15)
        passed = count == 0
        return CheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=f"Security: {count} eslint-security finding(s)",
            severity=Severity.ERROR if not passed else Severity.INFO,
            score=score,
            details={"finding_count": count},
            fix_hint="Address the eslint-plugin-security findings" if count else None,
        )
args property

Run ESLint over the project with the JSON formatter.

rule_id property

Unique identifier (shared with the Python security rule).

score_output(parsed, project_path)

Score by the count of eslint-plugin-security findings.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/quality.py
Python
def score_output(self, parsed: object, project_path: Path) -> CheckResult:
    """Score by the count of eslint-plugin-security findings."""
    count = _count_security_messages(parsed) if isinstance(parsed, list) else 0
    score = max(0, 100 - count * 15)
    passed = count == 0
    return CheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=f"Security: {count} eslint-security finding(s)",
        severity=Severity.ERROR if not passed else Severity.INFO,
        score=score,
        details={"finding_count": count},
        fix_hint="Address the eslint-plugin-security findings" if count else None,
    )

NodeStructureRule

Bases: ProjectRule

Check package.json completeness + tsconfig strict mode.

Mirrors the Python PyprojectCompletenessRule: binary field-presence checks, 100 - missing * 10.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/structure.py
Python
@register_rule("structure", framework=Framework.NODE)
class NodeStructureRule(ProjectRule):
    """Check package.json completeness + tsconfig strict mode.

    Mirrors the Python ``PyprojectCompletenessRule``: binary field-presence
    checks, ``100 - missing * 10``.
    """

    @property
    def rule_id(self) -> str:
        """Unique identifier for this rule."""
        return "STRUCTURE_PACKAGE_JSON"

    def check(self, project_path: Path) -> CheckResult:
        """Score by the count of missing manifest fields + strict tsconfig opts."""
        pkg = _load_json(project_path / "package.json")
        if pkg is None:
            return CheckResult(
                rule_id=self.rule_id,
                passed=False,
                message="package.json missing or unparsable",
                severity=Severity.ERROR,
                score=0,
                fix_hint="Create a valid package.json (npm init).",
            )
        missing = _missing_package_fields(pkg) + _missing_tsconfig(project_path)
        score = max(0, 100 - len(missing) * 10)
        passed = not missing
        return CheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=(
                "package.json + tsconfig complete"
                if passed
                else f"Missing: {', '.join(missing)}"
            ),
            severity=Severity.WARNING if not passed else Severity.INFO,
            score=score,
            details={"missing": missing},
            fix_hint=f"Add {', '.join(missing)}" if missing else None,
        )
rule_id property

Unique identifier for this rule.

check(project_path)

Score by the count of missing manifest fields + strict tsconfig opts.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/structure.py
Python
def check(self, project_path: Path) -> CheckResult:
    """Score by the count of missing manifest fields + strict tsconfig opts."""
    pkg = _load_json(project_path / "package.json")
    if pkg is None:
        return CheckResult(
            rule_id=self.rule_id,
            passed=False,
            message="package.json missing or unparsable",
            severity=Severity.ERROR,
            score=0,
            fix_hint="Create a valid package.json (npm init).",
        )
    missing = _missing_package_fields(pkg) + _missing_tsconfig(project_path)
    score = max(0, 100 - len(missing) * 10)
    passed = not missing
    return CheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=(
            "package.json + tsconfig complete"
            if passed
            else f"Missing: {', '.join(missing)}"
        ),
        severity=Severity.WARNING if not passed else Severity.INFO,
        score=score,
        details={"missing": missing},
        fix_hint=f"Add {', '.join(missing)}" if missing else None,
    )

NodeTestDuplicateRule

Bases: ProjectRule

Flag duplicate test bodies (identical it/test blocks).

Source code in packages/axm-audit/src/axm_audit/core/rules/node/test_quality.py
Python
@register_rule("test_quality", framework=Framework.NODE)
class NodeTestDuplicateRule(ProjectRule):
    """Flag duplicate test bodies (identical ``it``/``test`` blocks)."""

    @property
    def rule_id(self) -> str:
        """Unique identifier (shared with the Python duplicate-tests rule)."""
        return "TEST_QUALITY_DUPLICATE_TESTS"

    def check(self, project_path: Path) -> CheckResult:
        """Count test cases whose normalized body duplicates an earlier one."""
        if not (project_path / "package.json").is_file():
            return _no_package_json(project_path, self.rule_id)
        seen: set[str] = set()
        duplicates = 0
        for test in _all_test_files(project_path):
            text = test.read_text(encoding="utf-8", errors="replace")
            for match in _TEST_CASE.finditer(text):
                body = _normalize_body(match.group("body"))
                if len(body) < _MIN_BODY_LEN:
                    continue
                if body in seen:
                    duplicates += 1
                else:
                    seen.add(body)
        # Duplicate test bodies are zero-tolerance: any duplicate fails.
        score = max(0, 100 - duplicates * 10)
        passed = duplicates == 0
        return CheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=(
                "No duplicate test bodies"
                if not duplicates
                else f"{duplicates} duplicate test body(ies)"
            ),
            severity=Severity.WARNING if not passed else Severity.INFO,
            score=score,
            details={"duplicate_count": duplicates},
            fix_hint="Merge or parametrize duplicate tests" if duplicates else None,
        )
rule_id property

Unique identifier (shared with the Python duplicate-tests rule).

check(project_path)

Count test cases whose normalized body duplicates an earlier one.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/test_quality.py
Python
def check(self, project_path: Path) -> CheckResult:
    """Count test cases whose normalized body duplicates an earlier one."""
    if not (project_path / "package.json").is_file():
        return _no_package_json(project_path, self.rule_id)
    seen: set[str] = set()
    duplicates = 0
    for test in _all_test_files(project_path):
        text = test.read_text(encoding="utf-8", errors="replace")
        for match in _TEST_CASE.finditer(text):
            body = _normalize_body(match.group("body"))
            if len(body) < _MIN_BODY_LEN:
                continue
            if body in seen:
                duplicates += 1
            else:
                seen.add(body)
    # Duplicate test bodies are zero-tolerance: any duplicate fails.
    score = max(0, 100 - duplicates * 10)
    passed = duplicates == 0
    return CheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=(
            "No duplicate test bodies"
            if not duplicates
            else f"{duplicates} duplicate test body(ies)"
        ),
        severity=Severity.WARNING if not passed else Severity.INFO,
        score=score,
        details={"duplicate_count": duplicates},
        fix_hint="Merge or parametrize duplicate tests" if duplicates else None,
    )

NodeTestMirrorRule

Bases: ProjectRule

Every source module has a colocated *.test.ts (node mirror idiom).

Source code in packages/axm-audit/src/axm_audit/core/rules/node/test_quality.py
Python
@register_rule("practices", framework=Framework.NODE)
class NodeTestMirrorRule(ProjectRule):
    """Every source module has a colocated ``*.test.ts`` (node mirror idiom)."""

    @property
    def rule_id(self) -> str:
        """Unique identifier (shared with the Python mirror rule)."""
        return "PRACTICE_TEST_MIRROR"

    def check(self, project_path: Path) -> CheckResult:
        """Flag source modules with no sibling test file."""
        src = _src_dir(project_path)
        if src is None:
            return _no_package_json(project_path, self.rule_id)
        missing: list[str] = []
        for source in _iter_source_files(src):
            if source.stem in _MIRROR_EXEMPT_STEMS:
                continue
            if not self._has_sibling_test(source):
                missing.append(source.relative_to(src).as_posix())
        score = max(0, 100 - len(missing) * 15)
        passed = score >= PASS_THRESHOLD
        return CheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=(
                "All source modules have colocated tests"
                if not missing
                else f"{len(missing)} module(s) without a colocated test"
            ),
            severity=Severity.WARNING if not passed else Severity.INFO,
            score=score,
            details={"missing": missing[:20]},
            fix_hint=(
                f"Add colocated tests: {', '.join(missing[:5])}" if missing else None
            ),
        )

    @staticmethod
    def _has_sibling_test(source: Path) -> bool:
        """Return True if a ``<stem>.test.ts``/``.spec.ts`` sits beside *source*."""
        stem = source.stem
        for suffix in _TEST_SUFFIXES:
            if (source.parent / f"{stem}{suffix}").is_file():
                return True
        return False
rule_id property

Unique identifier (shared with the Python mirror rule).

check(project_path)

Flag source modules with no sibling test file.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/test_quality.py
Python
def check(self, project_path: Path) -> CheckResult:
    """Flag source modules with no sibling test file."""
    src = _src_dir(project_path)
    if src is None:
        return _no_package_json(project_path, self.rule_id)
    missing: list[str] = []
    for source in _iter_source_files(src):
        if source.stem in _MIRROR_EXEMPT_STEMS:
            continue
        if not self._has_sibling_test(source):
            missing.append(source.relative_to(src).as_posix())
    score = max(0, 100 - len(missing) * 15)
    passed = score >= PASS_THRESHOLD
    return CheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=(
            "All source modules have colocated tests"
            if not missing
            else f"{len(missing)} module(s) without a colocated test"
        ),
        severity=Severity.WARNING if not passed else Severity.INFO,
        score=score,
        details={"missing": missing[:20]},
        fix_hint=(
            f"Add colocated tests: {', '.join(missing[:5])}" if missing else None
        ),
    )

NodeTestPyramidRule

Bases: ProjectRule

Colocated *.test.ts are unit tests; flag ones doing real I/O.

A colocated unit test that touches the filesystem/network/subprocess is a soft signal it belongs in tests/integration or tests/e2e instead.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/test_quality.py
Python
@register_rule("test_quality", framework=Framework.NODE)
class NodeTestPyramidRule(ProjectRule):
    """Colocated ``*.test.ts`` are unit tests; flag ones doing real I/O.

    A colocated unit test that touches the filesystem/network/subprocess is a
    soft signal it belongs in ``tests/integration`` or ``tests/e2e`` instead.
    """

    @property
    def rule_id(self) -> str:
        """Unique identifier (shared with the Python pyramid-level rule)."""
        return "TEST_QUALITY_PYRAMID_LEVEL"

    def check(self, project_path: Path) -> CheckResult:
        """Flag colocated unit tests that perform real I/O."""
        src = _src_dir(project_path)
        if src is None:
            return _no_package_json(project_path, self.rule_id)
        misplaced: list[str] = []
        for test in self._colocated_tests(src):
            text = test.read_text(encoding="utf-8", errors="replace")
            if _REAL_IO.search(text):
                misplaced.append(test.relative_to(src).as_posix())
        score = max(0, 100 - len(misplaced) * 15)
        passed = score >= PASS_THRESHOLD
        return CheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=(
                "Colocated tests are pure unit tests"
                if not misplaced
                else f"{len(misplaced)} colocated unit test(s) do real I/O"
            ),
            severity=Severity.WARNING if not passed else Severity.INFO,
            score=score,
            details={"misplaced": misplaced[:20]},
            fix_hint=(
                "Move I/O tests to tests/integration or tests/e2e"
                if misplaced
                else None
            ),
        )

    @staticmethod
    def _colocated_tests(src: Path) -> list[Path]:
        """List ``*.test.ts``/``*.spec.ts`` files colocated in the source tree."""
        return sorted(p for p in _iter_files(src) if _is_test_file(p))
rule_id property

Unique identifier (shared with the Python pyramid-level rule).

check(project_path)

Flag colocated unit tests that perform real I/O.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/test_quality.py
Python
def check(self, project_path: Path) -> CheckResult:
    """Flag colocated unit tests that perform real I/O."""
    src = _src_dir(project_path)
    if src is None:
        return _no_package_json(project_path, self.rule_id)
    misplaced: list[str] = []
    for test in self._colocated_tests(src):
        text = test.read_text(encoding="utf-8", errors="replace")
        if _REAL_IO.search(text):
            misplaced.append(test.relative_to(src).as_posix())
    score = max(0, 100 - len(misplaced) * 15)
    passed = score >= PASS_THRESHOLD
    return CheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=(
            "Colocated tests are pure unit tests"
            if not misplaced
            else f"{len(misplaced)} colocated unit test(s) do real I/O"
        ),
        severity=Severity.WARNING if not passed else Severity.INFO,
        score=score,
        details={"misplaced": misplaced[:20]},
        fix_hint=(
            "Move I/O tests to tests/integration or tests/e2e"
            if misplaced
            else None
        ),
    )

NodeTestRule

Bases: NodeToolRule

Run Vitest and require a non-empty, fully-passing suite.

Mirrors the Python testing invariant: a green run with zero tests is NOT a pass (guards against passWithNoTests). Score is the pass ratio times 100, forced to 0 when no tests ran.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/testing.py
Python
@register_rule("testing", framework=Framework.NODE)
class NodeTestRule(NodeToolRule):
    """Run Vitest and require a non-empty, fully-passing suite.

    Mirrors the Python testing invariant: a green run with zero tests is NOT a
    pass (guards against ``passWithNoTests``). Score is the pass ratio times
    100, forced to 0 when no tests ran.
    """

    binary = "vitest"
    install_hint = "Install Vitest: npm install -D vitest"

    @property
    def rule_id(self) -> str:
        """Unique identifier (shared cross-framework: test-suite health)."""
        return "QUALITY_TESTS"

    @property
    def args(self) -> list[str]:
        """Run the suite once with the JSON reporter (no watch)."""
        return ["run", "--reporter=json"]

    @property
    def findings_returncodes(self) -> frozenset[int]:
        """Vitest exits 1 when tests fail — a finding we score, not a crash."""
        return frozenset({1})

    def score_output(self, parsed: object, project_path: Path) -> CheckResult:
        """Score by pass ratio; an empty suite is a hard fail (false-green guard)."""
        total, passed_count, failed = _test_counts(parsed)
        if total == 0:
            return CheckResult(
                rule_id=self.rule_id,
                passed=False,
                message="No tests ran (numTotalTests == 0)",
                severity=Severity.ERROR,
                score=0,
                details={"total": 0},
                fix_hint="Add tests — an empty suite never passes this gate.",
            )
        score = round(passed_count / total * 100)
        passed = failed == 0
        return CheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=f"Tests: {passed_count}/{total} passed",
            severity=Severity.ERROR if not passed else Severity.INFO,
            score=score,
            details={"total": total, "passed": passed_count, "failed": failed},
            fix_hint="Fix the failing tests above" if failed else None,
        )
args property

Run the suite once with the JSON reporter (no watch).

findings_returncodes property

Vitest exits 1 when tests fail — a finding we score, not a crash.

rule_id property

Unique identifier (shared cross-framework: test-suite health).

score_output(parsed, project_path)

Score by pass ratio; an empty suite is a hard fail (false-green guard).

Source code in packages/axm-audit/src/axm_audit/core/rules/node/testing.py
Python
def score_output(self, parsed: object, project_path: Path) -> CheckResult:
    """Score by pass ratio; an empty suite is a hard fail (false-green guard)."""
    total, passed_count, failed = _test_counts(parsed)
    if total == 0:
        return CheckResult(
            rule_id=self.rule_id,
            passed=False,
            message="No tests ran (numTotalTests == 0)",
            severity=Severity.ERROR,
            score=0,
            details={"total": 0},
            fix_hint="Add tests — an empty suite never passes this gate.",
        )
    score = round(passed_count / total * 100)
    passed = failed == 0
    return CheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=f"Tests: {passed_count}/{total} passed",
        severity=Severity.ERROR if not passed else Severity.INFO,
        score=score,
        details={"total": total, "passed": passed_count, "failed": failed},
        fix_hint="Fix the failing tests above" if failed else None,
    )

NodeTestTautologyRule

Bases: ProjectRule

Flag tautological assertions (expect(true), x.toBe(x)).

Source code in packages/axm-audit/src/axm_audit/core/rules/node/test_quality.py
Python
@register_rule("test_quality", framework=Framework.NODE)
class NodeTestTautologyRule(ProjectRule):
    """Flag tautological assertions (``expect(true)``, ``x.toBe(x)``)."""

    @property
    def rule_id(self) -> str:
        """Unique identifier (shared with the Python tautology rule)."""
        return "TEST_QUALITY_TAUTOLOGY"

    def check(self, project_path: Path) -> CheckResult:
        """Count tautological assertions across the project's test files."""
        if not (project_path / "package.json").is_file():
            return _no_package_json(project_path, self.rule_id)
        hits: list[str] = []
        for test in _all_test_files(project_path):
            text = test.read_text(encoding="utf-8", errors="replace")
            for lineno, line in enumerate(text.splitlines(), start=1):
                if any(pat.search(line) for pat in _TAUTOLOGY_PATTERNS):
                    rel = test.relative_to(project_path).as_posix()
                    hits.append(f"{rel}:{lineno}")
        # Tautologies are zero-tolerance: any occurrence fails (like the
        # Python tautology rule), score grades severity.
        score = max(0, 100 - len(hits) * 10)
        passed = not hits
        return CheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=(
                "No tautological assertions"
                if not hits
                else f"{len(hits)} tautological assertion(s)"
            ),
            severity=Severity.WARNING if not passed else Severity.INFO,
            score=score,
            details={"tautologies": hits[:20]},
            fix_hint="Replace weak asserts with behavioral ones" if hits else None,
        )
rule_id property

Unique identifier (shared with the Python tautology rule).

check(project_path)

Count tautological assertions across the project's test files.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/test_quality.py
Python
def check(self, project_path: Path) -> CheckResult:
    """Count tautological assertions across the project's test files."""
    if not (project_path / "package.json").is_file():
        return _no_package_json(project_path, self.rule_id)
    hits: list[str] = []
    for test in _all_test_files(project_path):
        text = test.read_text(encoding="utf-8", errors="replace")
        for lineno, line in enumerate(text.splitlines(), start=1):
            if any(pat.search(line) for pat in _TAUTOLOGY_PATTERNS):
                rel = test.relative_to(project_path).as_posix()
                hits.append(f"{rel}:{lineno}")
    # Tautologies are zero-tolerance: any occurrence fails (like the
    # Python tautology rule), score grades severity.
    score = max(0, 100 - len(hits) * 10)
    passed = not hits
    return CheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=(
            "No tautological assertions"
            if not hits
            else f"{len(hits)} tautological assertion(s)"
        ),
        severity=Severity.WARNING if not passed else Severity.INFO,
        score=score,
        details={"tautologies": hits[:20]},
        fix_hint="Replace weak asserts with behavioral ones" if hits else None,
    )

NodeTypeCheckRule

Bases: NodeToolRule

Run tsc --noEmit and score by the TypeScript error count.

Scoring: 100 - errors * 5, min 0 — identical to the Python type rule.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/typecheck.py
Python
@register_rule("type", framework=Framework.NODE)
class NodeTypeCheckRule(NodeToolRule):
    """Run ``tsc --noEmit`` and score by the TypeScript error count.

    Scoring: ``100 - errors * 5``, min 0 — identical to the Python type rule.
    """

    binary = "tsc"
    install_hint = "Install TypeScript: npm install -D typescript"

    @property
    def rule_id(self) -> str:
        """Unique identifier for this rule (shared with the Python type rule)."""
        return "QUALITY_TYPE"

    @property
    def args(self) -> list[str]:
        """Type-check without emitting, with a parseable (non-pretty) format."""
        return ["--noEmit", "--pretty", "false"]

    @property
    def findings_returncodes(self) -> frozenset[int]:
        """tsc exits 1 or 2 when it *found* type errors — not an env failure."""
        return frozenset({1, 2})

    def parse(self, result: subprocess.CompletedProcess[str]) -> object:
        """tsc emits text, not JSON — return raw stdout for scoring."""
        return result.stdout

    def score_output(self, parsed: object, project_path: Path) -> CheckResult:
        """Score by the count of ``error TSxxxx`` diagnostics."""
        stdout = parsed if isinstance(parsed, str) else ""
        error_count = _count_type_errors(stdout)
        score = max(0, 100 - error_count * 5)
        passed = score >= PASS_THRESHOLD
        return CheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=f"Type score: {score}/100 ({error_count} errors)",
            severity=Severity.WARNING if not passed else Severity.INFO,
            score=score,
            details={"error_count": error_count},
            fix_hint="Fix the tsc errors above" if error_count > 0 else None,
        )
args property

Type-check without emitting, with a parseable (non-pretty) format.

findings_returncodes property

tsc exits 1 or 2 when it found type errors — not an env failure.

rule_id property

Unique identifier for this rule (shared with the Python type rule).

parse(result)

tsc emits text, not JSON — return raw stdout for scoring.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/typecheck.py
Python
def parse(self, result: subprocess.CompletedProcess[str]) -> object:
    """tsc emits text, not JSON — return raw stdout for scoring."""
    return result.stdout
score_output(parsed, project_path)

Score by the count of error TSxxxx diagnostics.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/typecheck.py
Python
def score_output(self, parsed: object, project_path: Path) -> CheckResult:
    """Score by the count of ``error TSxxxx`` diagnostics."""
    stdout = parsed if isinstance(parsed, str) else ""
    error_count = _count_type_errors(stdout)
    score = max(0, 100 - error_count * 5)
    passed = score >= PASS_THRESHOLD
    return CheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=f"Type score: {score}/100 ({error_count} errors)",
        severity=Severity.WARNING if not passed else Severity.INFO,
        score=score,
        details={"error_count": error_count},
        fix_hint="Fix the tsc errors above" if error_count > 0 else None,
    )

NodeVulnerabilityRule

Bases: NodeToolRule

Score npm-audit vulnerabilities (HIGH/CRITICAL).

Mirrors the Python DEPS_AUDIT: 100 - (high+critical) * 15. Lives in the deps category like its Python counterpart.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/security.py
Python
@register_rule("deps", framework=Framework.NODE)
class NodeVulnerabilityRule(NodeToolRule):
    """Score npm-audit vulnerabilities (HIGH/CRITICAL).

    Mirrors the Python ``DEPS_AUDIT``: ``100 - (high+critical) * 15``. Lives in
    the ``deps`` category like its Python counterpart.
    """

    binary = "npm"
    on_path = True
    install_hint = "npm is required to run `npm audit`"

    @property
    def rule_id(self) -> str:
        """Unique identifier (shared with the Python dependency-audit rule)."""
        return "DEPS_AUDIT"

    @property
    def args(self) -> list[str]:
        """Full vulnerability report as JSON (no --audit-level: that's a gate)."""
        return ["audit", "--json"]

    @property
    def findings_returncodes(self) -> frozenset[int]:
        """npm audit exits 1 when vulnerabilities are present — a finding."""
        return frozenset({1})

    def score_output(self, parsed: object, project_path: Path) -> CheckResult:
        """Score by HIGH (15 each) + CRITICAL (15 each) vulnerability counts."""
        high, critical = _vuln_counts(parsed)
        total = high + critical
        score = max(0, 100 - total * 15)
        passed = total == 0
        return CheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=f"Vulnerabilities: {critical} critical, {high} high",
            severity=Severity.ERROR if not passed else Severity.INFO,
            score=score,
            details={"high": high, "critical": critical},
            fix_hint="Run: npm audit fix" if total else None,
        )
args property

Full vulnerability report as JSON (no --audit-level: that's a gate).

findings_returncodes property

npm audit exits 1 when vulnerabilities are present — a finding.

rule_id property

Unique identifier (shared with the Python dependency-audit rule).

score_output(parsed, project_path)

Score by HIGH (15 each) + CRITICAL (15 each) vulnerability counts.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/security.py
Python
def score_output(self, parsed: object, project_path: Path) -> CheckResult:
    """Score by HIGH (15 each) + CRITICAL (15 each) vulnerability counts."""
    high, critical = _vuln_counts(parsed)
    total = high + critical
    score = max(0, 100 - total * 15)
    passed = total == 0
    return CheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=f"Vulnerabilities: {critical} critical, {high} high",
        severity=Severity.ERROR if not passed else Severity.INFO,
        score=score,
        details={"high": high, "critical": critical},
        fix_hint="Run: npm audit fix" if total else None,
    )