Contributing¶
Setup¶
git clone https://github.com/aitmlouk/mcdakit
cd mcdakit
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev,compare,docs]"
Branches¶
main is the release branch: every commit on it is a state that could be
tagged. dev is where work happens.
git switch dev
# ... work, commit ...
git push origin dev
CI runs the full matrix on both, so dev gets the same gate as main rather
than deferring every failure to merge time. The documentation builds on both
too — a broken -W build fails on dev before it can reach main — but only
main publishes the site, so development pushes cannot overwrite what readers
see.
When dev is ready:
git switch main
git merge --no-ff dev # keeps the merge visible in the history
git push origin main
Release from main: bump the version in src/mcdakit/__init__.py (the single
source of truth; CITATION.cff is checked against it by the tests), move the
[Unreleased] changelog entries under the new version, tag, and publish.
Where things live¶
src/mcdakit/
types.py Criterion, Decision, Result — validated at construction
orientation.py Cost criteria mirrored to benefit, once, centrally
normalization.py Normalisation schemes and their registry
weighting.py Objective weight derivation (entropy, CRITIC, ...)
ranking.py rank(), compare_methods(), agreement, reversal_check
sensitivity.py How far a weight can move before the winner changes
explain.py Why an alternative won, and by what margin
ahp.py Pairwise comparison: weights, and full AHP ranking
group.py Several stakeholders, and where they disagree
ai.py Optional LLM assistance; imported explicitly, never
re-exported from the package namespace
testing.py Conformance suite that method authors run
methods/ One module per method — see below
One method per module, named for the method: topsis.py holds topsis
and Topsis. Two exceptions, each because the methods share an
implementation rather than merely a theme — scoring.py (simple scoring,
weighted scoring and SAW differ only in weighting and normalisation) and
waspas.py (WPM is WASPAS at lambda zero and calls it, so splitting them
would let the two drift apart). Genuinely shared code goes in
methods/_common.py or methods/base.py, never in a module that happens to
hold a related method.
Tests mirror this: tests/test_topsis.py for topsis.py. benchmarks/,
examples/ and docs/ are not shipped in the wheel.
The checks CI runs¶
pytest -q # the suite, including doctests
pytest -q --cov=mcdakit # must stay at 100% (line and branch)
ruff check . # lint
ruff format --check . # formatting
mypy # types
vulture src/ .vulture-allowlist.py --min-confidence 60 # dead code
python benchmarks/reversal.py # SPOTIS must measure 0.0%
python benchmarks/comparison.py # against pymcdm et al. (needs the extras)
python benchmarks/crosscheck.py # every method vs every rival that has it
All of these must pass. ruff format . fixes formatting in place.
While developing you will usually want a subset. The whole suite takes about four seconds, so reach for these mainly to read the output, not for speed:
pytest tests/test_normalization.py # one file
pytest tests/test_normalization.py::TestDefinitions # one class
pytest -k reversal # anything matching a keyword
pytest -x -q # stop at the first failure
pytest --lf # only what failed last time
If pytest reports ModuleNotFoundError: No module named 'mcdakit', the
virtualenv is not active — activate it, or call it explicitly with
.venv/bin/python -m pytest. The src/ layout means the package is only
importable once installed, which is deliberate: it stops a stale working-tree
copy shadowing the one under test.
Coverage is enforced at 100%, line and branch. A line nothing exercises is
a line nobody has checked. Where code is genuinely unreachable — a guard
against the library’s own plumbing breaking, a fallback a validated type makes
impossible — mark it # pragma: no cover with a comment saying why, and
never by lowering the threshold. There are 14 such pragmas today; each names
its reason.
If vulture flags something that is used — via a test, a doc example, or a
plugin — add it to .vulture-allowlist.py with the reason. An allowlist
of unexplained names stops being a check.
Documentation¶
The site is Sphinx with MyST, so pages are Markdown. Build it locally:
pip install -e ".[docs]"
sphinx-build -b html -W docs docs/_build/html
open docs/_build/html/index.html
-W turns warnings into errors, which is what CI and the hosting build both
use. A broken cross-reference fails the build rather than shipping a dead link.
Where things go:
Page |
For |
|---|---|
|
Someone who has just installed it |
|
Rank reversal, SPOTIS, sensitivity — the why |
|
Writing and shipping a method |
|
Generated reference; add an |
|
Decisions with consequences, and their costs |
|
Theme refinements — see below |
The theme is Furo, which ships light and dark modes with a toggle in the
header (and follows the reader’s system setting by default). custom.css
refines it; every rule there uses Furo’s own CSS variables rather than
hardcoded colours, because a literal hex will be wrong in one of the two
modes. If you add a rule, check it in both.
Two rules the tests enforce: every name in mcdakit.__all__ must appear in
docs/api.md, and every public callable needs a docstring. A name nobody can
find in the docs is a name nobody uses.
Hosting¶
The site is published to GitHub Pages at
https://aitmlouk.github.io/mcdakit by .github/workflows/docs.yml, which
builds on every push to main with warnings as errors.
Deployment is gated on the repository being public, so while it is private the workflow builds and stops. It starts publishing by itself once the repository is made public and Pages is enabled under Settings → Pages → Source: GitHub Actions.
A Read the Docs configuration was committed alongside this one and has been removed. Two hosts mean two URLs that drift apart, and readers finding whichever ranks higher in search; if you ever prefer Read the Docs — it offers versioned builds and PDF output, which this does not — delete the workflow at the same time as adding the configuration back.
House rules¶
These are not style preferences; each one is load-bearing.
1. numpy only at runtime. No pandas, no scipy, no sklearn. A single tiny dependency is a large part of why someone picks this over a heavier alternative. If something seems to need scipy, write the twenty lines. There is a test that enforces this.
2. Never silently degrade. Missing bounds, zero-variance criteria, ties, all-zero weights, an argument nobody reads: warn or raise, never quietly produce a number that looks authoritative. This package’s whole claim is trustworthiness, and a confident wrong answer is worse than an error.
3. Cite the maths. Every method’s docstring names its source. This library will be read by people who know the papers.
4. Verify numbers before publishing them. Any figure in a docstring, README or docs page must be produced by running the code, not recalled. Recalled figures were wrong three times out of three during the initial build.
5. Higher is always better. Every method returns scores where larger ranks higher, whatever its native convention. Negate in the method, document it, and test it.
Adding a method¶
Please read the extending guide (docs/extending.md) first — you may not need to change this
package at all. A method distributed as a plugin is a first-class citizen:
reachable by name, included in compare_methods(), analysable by
sensitivity().
Whether it lives here or in your own package, check it first:
python -m mcdakit.testing my_package.methods:MyMethod
That runs the conformance suite — thirteen properties every sound method holds. It catches the failures that are otherwise silent: a score convention that ranks everything backwards, a NaN on a criterion every option scores alike, criterion direction handled twice. Call it from your own tests too:
from mcdakit.testing import check_method
def test_my_method_conforms():
check_method(MyMethod())
Every method shipped here passes it, and CI enforces that — holding contributors to a standard the library itself failed would be indefensible.
Passing is a floor, not a ceiling: it says your method is well-behaved, not that its arithmetic matches the paper.
If it does belong here, it needs:
a
Methodsubclass withname,summaryandcitation;the algorithm as a plain function over numpy arrays, so it can be tested and imported on its own;
python -m mcdakit.testingpassing;tests against values verified outside this codebase — a published paper, a hand computation, or a cross-check against another library;
an entry in the README’s method table.
Tests¶
New behaviour needs a test that would fail without it. Beyond that:
Prefer values checked independently over values the code happened to produce. A fixture recording current output passes forever, including after a regression.
When a result is surprising but correct — VIKOR picking a different winner from every additive method, say — write the test that documents why, not a test that hides it.
Property-based tests live in
tests/test_properties.py. Invariants (dominance, scale invariance, permutation invariance) belong there.
Changing a published number¶
If a benchmark figure moves, update the README, the docs, and CHANGELOG.md
together, and say in the commit message what changed and why. A stale number
in the README is a correctness bug.