Two extension points exist: analyzers (produce evidence) and reporters (render results). Both are discovered via standard Python entry points — no changes to this package are needed to add one, and a plugin is just an ordinary pip-installable package.
An analyzer is any class satisfying this shape (crie/analyzers/base.py):
from collections.abc import Iterable
from typing import Any, ClassVar
from crie.core.models import Category, Domain, Evidence, Language
from crie.core.repository import AnalysisContext
class MyAnalyzer:
name: ClassVar[str] = "my_analyzer"
supported_languages: ClassVar[frozenset[Language]] = frozenset() # empty = language-agnostic
def __init__(self, options: dict[str, Any] | None = None) -> None:
self.options = options or {}
def analyze(self, context: AnalysisContext) -> Iterable[Evidence]:
for file in context.files:
yield Evidence(
analyzer=self.name,
file_path=file.relative_path,
category=Category.COMPLEXITY, # feeds the risk dimensions; pick one that fits
domain=Domain.MAINTAINABILITY, # feeds software evaluation; optional, set either/both/neither
metric="my_metric",
raw_value=42.0, # HIGHER MUST MEAN RISKIER (see below)
unit="widgets",
explanation="42 widgets found",
)Notes:
- No subclassing is required —
Analyzeris a structuralProtocol. Any object with aname,supported_languages, andanalyze()satisfies it. raw_valuemust be defined so that a higher number means higher risk, within its own metric's distribution. The normalizer and both aggregators treat every metric identically based on this convention; a metric that inverts it (e.g. "test coverage percentage" instead of "coverage gap") will silently score backwards. If a metric genuinely has no consistent risk direction (seearchitecture.py'sinstability, which is deliberately informational), leave bothcategoryanddomainunset — it still gets normalized and shown, just never averaged into a score.context.filesis the already-filtered, already-language-detected file list;context.repository.git()gives you the shared, cachedGitRepositoryif you need git history — reuse it rather than shelling out independently.- Raise nothing you don't want to abort the whole run over: an analyzer that raises
is caught by the pipeline and recorded in
metadata.analyzer_errors, but the file it was analyzing gets none of its evidence. Prefer catching your own expected failure modes (bad syntax, missing files) and simply yielding no evidence for that file. - Pick an existing
Categoryif one fits (see scoring-model.md) and/or an existingDomainif one fits (see software-evaluation.md); introducing a new one means it won't be included in any built-in weight preset until you also add config overrides for it. context.profile(aProjectProfile) is available if a finding should be suppressed or reframed for certain project types — seedebuggability.py'sprint_debuggingcheck for a worked example.
In your plugin package's pyproject.toml:
[project.entry-points."crie.analyzers"]
my_analyzer = "my_package.analyzers:MyAnalyzer"Once your package is installed in the same environment as crie, it shows up
automatically in crie list-analyzers and can be enabled/configured like any
built-in analyzer via crie.config.yaml's analyzers.my_analyzer block.
A plugin that fails to import is skipped with a logged warning
(crie/analyzers/registry.py::discover_analyzers) — one broken plugin doesn't take
down analysis for everyone else.
Same pattern, simpler protocol (crie/reporters/base.py):
from typing import ClassVar
from crie.core.models import AnalysisResult
from crie.reporters.base import Reporter
class MyReporter(Reporter): # subclassing gets you the default write() for free
name: ClassVar[str] = "my_format"
file_extension: ClassVar[str] = "txt"
def render(self, result: AnalysisResult, top_n: int = 25) -> str:
return f"{result.repository.name}: {len(result.files)} files analyzed\n"Subclassing Reporter is optional but gets you write() (creates the output
directory, writes report.<file_extension>) for free — you only need to implement
render(). Register it the same way:
[project.entry-points."crie.reporters"]
my_format = "my_package.reporters:MyReporter"It then becomes available via --format my_format on crie analyze and crie report.
If you're developing against a local checkout of this repo, tests/support/repo_builder.py
has a small GitRepoBuilder you can reuse to build a throwaway git repository with
controlled, backdated commit history in a test — see tests/conftest.py's
crafted_repo fixture for a worked example.