Architecture
JOSSeph is a layered Python pipeline with a Docker-first execution model.
Module map
josseph/
├── domain/ — value objects (RepositoryRef, AnalysisTarget)
├── providers/ — external API clients (GitHub, SonarQube)
├── metrics/
│ ├── abstract_extractor.py — MetricExtractor ABC
│ ├── registry.py — ExtractorRegistry (auto-discovery)
│ └── extractors/ — concrete extractor modules (ck, cm, github, sonar)
├── pipeline/
│ ├── config.py — YAML loading and validation → AnalysisConfig
│ ├── repositories.py — repository list parsing
│ ├── cloner.py — git clone / cleanup (RepositoryCloner)
│ ├── analyzer.py — per-repository orchestration (RepositoryAnalyzer)
│ ├── runner.py — parallel execution (AnalysisRunner, ThreadPoolExecutor)
│ ├── results.py — artifact writing (ResultWriter, ResultDirectoryManager)
│ ├── run_report.py — run summary collection (RunReportCollector)
│ ├── extractor_factory.py — extractor selection from registry
│ └── app.py — pipeline entrypoint (RepositoryAnalysisPipeline)
├── process.py — CommandRunner protocol + SubprocessCommandRunner
├── utils.py — path constants, logging setup, HTTP retry helpers
└── __main__.py — CLI entrypoint (argparse)
Execution flow
docker compose run --rm josseph configs/config.yaml
│
▼
__main__.py parse args → RepositoryAnalysisPipeline.run(args)
│
▼
config.py load YAML → AnalysisConfig
resolve repositories file → list[RepositorySpec]
│
▼
registry.py auto-discover extractor modules via pkgutil.iter_modules
build_extractor() called for each selected tool
│
▼
runner.py ThreadPoolExecutor(workers)
for each repo ──► analyzer.py RepositoryAnalyzer.analyze(repo_spec)
│
├── checkout-free extractors (github) ──► extractor.run(target)
│
└── checkout-required extractors (ck, cm, sonar)
│
▼
cloner.py git clone → project_dir (temp, cleaned up after)
│
▼
git rev-parse HEAD → commit_hash
│
▼
extractor.run(target_with_checkout)
│
▼
results.py write <tool>.parquet + <tool>.json per extractor
│
▼
run_report.py write results/runs/<run-id>/summary.json
Key design decisions
Contract-driven caching
A cached result is valid only when both files are present:
results/<owner>@<repo>/<tool>.parquetresults/<owner>@<repo>/<tool>.json
If only one exists, the extractor reruns. This prevents partial writes from
being silently treated as complete. For revision-bound extractors with a
pinned commit, the commit_hash recorded in <tool>.json must additionally
match the requested commit, so changing a pinned commit invalidates the cache.
observation-bound extractors are cached by file presence only; the binding
distinction is documented for reproducibility.
Extractor isolation
Each extractor is a self-contained module under josseph/metrics/extractors/.
Failures are caught per-extractor and recorded in summary.json; the pipeline
continues with remaining extractors and repositories.
Protocol-based dependency injection
CommandRunner is a protocol (structural subtyping). Tests inject a fake
implementation without modifying production code. This also makes it possible
to swap out subprocess execution for other backends.
Checkout-free / checkout-required split
Extractors declare requires_checkout: bool. The analyzer splits them into two
groups and runs the checkout-free group before cloning. This avoids a full git
clone when only API-based extractors are needed.
Pinned repository commits are only supported when reachable from the repository's default branch. The clone step still checks out the requested commit after cloning.
Adding a new extractor
The extractor API contract is intentionally small. In short:
- Add
josseph/metrics/extractors/my_extractor.py - Define
EXTRACTOR_NAME,build_extractor(context, settings), and aMetricExtractorsubclass - Reference the name in
tools:in your config YAML
The registry discovers the module automatically via pkgutil.iter_modules.
No registration step is required.
Limitations
- Java-only: CK and CM filter for
.javafiles; SonarQube default exclusions drop non-Java code. - Single host: SonarQube runs on the same Docker network as the pipeline. There is no multi-host or cloud SonarQube support.
- Cache staleness: cached results are validated by artifact presence and, for
revision-bound extractors with a pinned commit, by the recorded commit hash —
not by content fingerprint. If you change tool versions or extractor
settings, clear
results/or use--force.