Contributing to ai-footprint
Technical guide: dev setup, conventions, code architecture, data schema, and
how to extend the project. For how impact is calculated (the exchanges
with EcoLogits, the methodology choices), see
METHODOLOGY.md.
Setup
git clone https://github.com/hrenaud/ai-footprint
cd ai-footprint
python3 -m venv .venv
.venv/bin/pip install -e . # installs ai-footprint + EcoLogits (tag 0.11.0)
.venv/bin/python -m pytest -q # the suite must be green
Run the CLI in dev: .venv/bin/python -m ai_footprint <command>.
Testing install.sh on a branch (before merging into main)
install.sh installs main by default, but accepts AI_FOOTPRINT_REF to
point at any branch or tag — useful to test a contribution under real
conditions (clone + venv + Claude Code hook) before merging:
AI_FOOTPRINT_REF=my-branch AI_FOOTPRINT_DIR=/tmp/ai-footprint-test \
curl -fsSL https://raw.githubusercontent.com/hrenaud/ai-footprint/main/install.sh | bash
AI_FOOTPRINT_DIR avoids overwriting the current installation in
~/.ai-footprint/src during the test. See also AI_FOOTPRINT_DB,
AI_FOOTPRINT_NO_CLAUDE, AI_FOOTPRINT_NO_INGEST at the top of
install.sh.
To clean up a test installation:
AI_FOOTPRINT_DIR=/tmp/ai-footprint-test AI_FOOTPRINT_PURGE_DB=1 bash uninstall.sh
(uninstall.sh undoes everything install.sh sets up; it uses the same
AI_FOOTPRINT_DIR / AI_FOOTPRINT_DB variables, plus
AI_FOOTPRINT_PURGE_DB=1 to also delete the database).
Conventions
- French for code (comments, docstrings) and user-facing messages.
- TDD: write the test, watch it fail, implement, watch it pass, commit.
- Semantic commits:
feat:,fix:,docs:,refactor:,perf:,test:,chore:. - Never delete a file with
rm— usetrash. - Simplicity first (YAGNI): the minimum code that solves the problem.
- EcoLogits parameters in billions everywhere (see METHODOLOGY).
Architecture
JSONL Claude Code (~/.claude/projects/**/*.jsonl)
↓
ClaudeCodeCollector (parse, normalize, active time, client)
↓
InferenceEvent[] (provider, model, tokens, timestamp, session, project, active_seconds, client)
↓
EcoLogitsEngine (offline, EcoLogits 0.11.0)
├─ recognized model → llm_impacts()
└─ otherwise → ModelParamsResolver + compute_llm_impacts()
↓
ImpactRecord (5 criteria min/max, usage/embodied phases, warnings, error)
↓
SQLiteStore (idempotent; events / impacts / sessions / pending_models)
↓
CLI: report · statusline · resolve · models (read the DB, never the JSONL)
Modules (ai_footprint/)
| Module | Role |
|---|---|
collectors/claude_code.py |
parses JSONL → InferenceEvent (ignores non-assistant/without usage; derives project from cwd; estimates active_seconds; fills client). No prompt/response content is extracted. |
models.py |
InferenceEvent dataclass. |
impact/engine.py |
EcoLogitsEngine.compute(): registry path vs. self-hosted fallback; _extract_impacts (totals/usage/embodied as min/max). |
impact/resolver.py |
ModelResolver: name aliases (Config.model_aliases). |
impact/params.py |
ModelParamsResolver (cascade registry→cache→HF→file) + fetch_hf_params(repo) (safetensors ÷ 1e9, offline-safe). |
store/db.py |
SQLiteStore: idempotent ingestion, aggregations, recompute. |
report/cli.py |
renders the report sections (5, plus a 6th, intensity per tool, if several tools are present). Also exposes _central/_scale/_ranked_projects, reused by card/cli.py. |
card/cli.py |
card subcommand: aggregates the totals (build_card_data), generates the HTML (render_card_html + card/template.html), renders the PNG via a local headless Chrome/Chromium (render_png, _find_chrome). |
resolve/cli.py |
resolve subcommand (list/set/recompute/forget). |
statusline/line.py |
compact line. |
dates.py |
parse_since() (normalizes --since dates). |
config.py |
Config dataclass (JSON ~/.ai-footprint/config.json). |
cache.py |
generic JSON cache throttled by TTL (load_json_cache/save_json_cache/should_refresh), reused by tool_updates.py and nudge.py. |
nudge.py |
proactive proposals: check_self_update (ai-footprint update via GitHub tag), check_uncovered_batch/mark_batch_prompted (resolving uncovered models, batch silence). |
__main__.py |
argument parser + command dispatch. |
Database schema (~/.ai-footprint/ai-footprint.db)
sqlite3, row_factory = Row, additive migrations via ALTER TABLE.
CREATE TABLE events (
session_id TEXT, msg_id TEXT,
provider TEXT, model TEXT,
input_tokens INTEGER, output_tokens INTEGER,
cache_creation_tokens INTEGER, cache_read_tokens INTEGER,
timestamp TEXT, -- ISO 8601
project TEXT, -- derived from cwd
active_seconds REAL DEFAULT 0, -- estimated active time (intensity)
client TEXT DEFAULT '', -- source tool (claude-code…)
PRIMARY KEY (session_id, msg_id)
);
CREATE TABLE impacts (
session_id TEXT, msg_id TEXT,
model_resolved TEXT, zone TEXT, methodology_version TEXT,
energy_min REAL, energy_max REAL, gwp_min REAL, gwp_max REAL,
adpe_min REAL, adpe_max REAL, pe_min REAL, pe_max REAL,
wcf_min REAL, wcf_max REAL,
breakdown_json TEXT, -- {"usage": {...}, "embodied": {...}}
warnings TEXT, error TEXT, -- non-NULL error = uncovered
PRIMARY KEY (session_id, msg_id)
);
CREATE TABLE sessions (session_id TEXT PRIMARY KEY, project TEXT, started_at TEXT, ended_at TEXT);
CREATE TABLE pending_models (provider TEXT, model TEXT, first_seen TEXT, occurrences INTEGER DEFAULT 0,
PRIMARY KEY (provider, model));
Idempotence: INSERT OR IGNORE on (session_id, msg_id); re-ingestion
does not recompute the impact but backfills missing active_seconds/client.
Key SQLiteStore methods (readable filtered by since, lexicographic
comparison on timestamp):
rows_for_report(since, session_id)— total / projects.tokens_by_model(since)— total tokens + central value & min/max bounds per criterion.session_count(since),first_session_started_at(),clients_covered(since)— used by the card (sub-hero, period label, covered tools).intensity_by_model(since)— active hours, tok/h, impact/h (events with time > 0).uncovered_by_model(since)— uncovered models (excluding<synthetic>).uncovered_keys()— uncovered(provider, model)pairs (excluding<synthetic>), without asincefilter; used byresolve --retry-hfand byai_footprint/nudge.py.coverage()—{total, measured, uncovered}.recompute_errors(engine, config)— recomputes events inerror→{before, after}.mark_model_events_error(provider, model, error)— puts a model back into error state (matching(session_id, msg_id)) to revert a mapping.
Separation of events / impacts
events = normalized raw source (immutable). impacts = calculation
result (depends on the engine + zone + params). This allows
recalculating without re-parsing the JSONL.
Card PNG: headless Chrome rather than Playwright
The HTML → PNG rendering of ai-footprint card drives an already
installed local Chrome/Chromium as a subprocess (--headless=new
--screenshot=...), not Playwright: zero new Python dependency, doesn't add
weight to the default installation (the statusline's Stop hook doesn't
need a browser). card/cli.py::_find_chrome() detects the binary
(CHROME_BIN, common macOS paths, then PATH); if absent, the command
fails cleanly with install instructions rather than crashing.
Tests
tests/ (pytest). Useful conventions:
- Deterministic offline: to force a Hugging Face lookup to fail without
network, use a model name containing
:(rejected by HF validation before any network call). For the success path, mockhuggingface_hub.model_infoviamonkeypatch.setitem(sys.modules, "huggingface_hub", fake)(seetest_params_huggingface.py). - Temporary config in CLI tests: monkeypatch
Config.load/Config.saveto atmp_pathpath (seetest_cli_models.py).
Run: .venv/bin/python -m pytest -q.
Documentation site (docs/guide)
The Markdown files in docs/ (METHODOLOGY.md,
comparaison-donnees-outils.md, publication-pypi.md,
checklist-nouvel-outil.md) are converted to HTML via MkDocs (+
mkdocs-static-i18n), independently of the landing pages
(docs/index.html, docs/fr/index.html) which remain hand-written.
- Config:
mkdocs.yml(docs_dir: docs,exclude_docsexcludes the landing pages/assets so MkDocs only touches the.mdfiles). - Bilingual: FR is the default locale (current content),
/en/is ready to host translations (file.en.md) — as long as they don't exist,/en/shows the FR content (mkdocs-static-i18nfallback). - GitHub Pages serves
docs/as-is (no server-side build): after any change to one of the Markdown files, regenerate and commit the result:
bash
.venv/bin/python scripts/build_docs.py
The script builds into a temporary folder then replaces docs/guide/
(the temporary .mkdocs-build/ folder is gitignored).
Extending
- New collector (a tool other than Claude Code): implement a collector
that emits
InferenceEvents (filling inprovider/client), on the model ofClaudeCodeCollector. The rest of the pipeline is neutral with respect to the source. Full checklist to follow for each integration:checklist-nouvel-outil.md. - New skill: add
skills/<name>/SKILL.md(frontmattername/description). The installer deploys it via a symlink into~/.claude/skills/. - Model resolution: the cascade lives in
impact/params.py; the deterministicresolveCLI (HF + recompute) inresolve/cli.py; the name→repo mapping (judgment call) in the/footprint-resolveskill. HF failures are memoized (in-memory negative cache + persisted inconfig.json, 7-day TTL);resolve --retry-hfpurges this cache and retries the cascade on the uncovered models. Params estimated from file size carry provenance warnings (params-bytes-per-param:<n>,params-range-unknown-dtype) and are flagged in the report.
Technical backlog
See
.superpowers/specs/2026-07-02-qualite-lecture-resolution-design.md:
data-reading and model-resolution quality fixes (HF negative cache, 4-bit
estimation…), and the "WebSearch step" evolution in the resolution
cascade. resolve --set "P/M=repo:<active>" handles MoE models.
Release
A release bumps the semantic version, generates the CHANGELOG, and creates the tag.
Always with the local venv binary (.venv/bin/ai-footprint), never the
global ai-footprint command: the latter runs the code of the installed
clone (~/.ai-footprint/src) and would commit/tag there instead of the dev
repo you're working in.
.venv/bin/ai-footprint release bump <patch|minor|major> [--no-push]
patch: backward-compatible fixesminor: backward-compatible new featuresmajor: incompatible changes
The process:
- Checks that the tree is clean, that we're on
main, and that the target tag doesn't already exist. - Computes the new version (e.g.
0.1.0→0.2.0). - Generates the CHANGELOG between the last
v*tag and HEAD from the conventional commits (feat:,fix:, etc.). - Bumps
pyproject.toml+ai_footprint/__init__.py. - Prepends the new block into
CHANGELOG.md. - Commits
chore(release): X.Y.Z+ tagsvX.Y.Z. - Pushes
origin main --tagsby default (--no-pushoption to skip it).
Evidence: the tests/test_release.py tests (31 tests) cover the full
cycle.
Note: before the first
v*tag, the CHANGELOG is maintained manually (the "Pre-versioning" section). After the first release, it is entirely auto-generated.
Dependency watch (ecologits, huggingface_hub)
A GitHub Actions workflow (.github/workflows/check-tool-updates.yml,
weekly cron + manual trigger) compares the versions pinned in
pyproject.toml (ecologits pinned exactly, huggingface_hub) against the
latest versions published on PyPI (via ai_footprint/tool_updates.py) and
opens an issue if a new version exists.
No automatic bump: ecologits is pinned to an exact PyPI version because
a minor 0.x bump can break the calculation cascade, and the installed
tool shares its database with the dev repo (see § Two codebases, one
base) — a silent bump would be risky. The issue is just a reminder; the
bump is done by hand in pyproject.toml after testing.
In addition to the weekly cron, a SessionStart hook local to the dev
repo (.claude/settings.json, not the global ~/.claude/settings.json
installed by install.sh) runs ai-footprint tool-updates-check on every
Claude Code session start in this project, and displays a message if an
ecologits/huggingface_hub update is available. The network check is cached
for 24h (.claude/tool-updates-cache.json, gitignored) so it doesn't slow
down every session start. This logic is tested in
tests/test_tool_updates.py (session_start_notice, should_refresh,
load_cache).
Not to be confused with the global
SessionStarthook added byinstall.shfor end-user nudges (ai-footprint nudge --claude-hook, seeai_footprint/nudge.py): that one is internal to the dev repo (.claude/settings.json, not installed for end users) and is used to alert ai-footprint maintainers about new ecologits/huggingface_hub releases — an entirely different topic from the nudges aimed at ai-footprint's end users.
Out of current scope (seams laid down)
Third-party collectors (Codex, local inference) as stubs; compute_live()
(real-time instrumentation) and import_legacy() not implemented; CSV/JSON
export and workstation energy are out of scope.