diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..e0ea6cc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,52 @@ +# Dependabot configuration for mmcontext +# Docs: https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file +# +# Security updates (PRs that fix Dependabot *alerts*) are enabled separately in +# Settings -> Code security and obey target-branch below. This file additionally +# enables proactive *version* updates on a schedule, grouped to reduce PR noise. +version: 2 +updates: + # Python dependencies declared in pyproject.toml + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + # PRs target the integration branch, never main, per the project branch strategy. + target-branch: "dev-claude" + open-pull-requests-limit: 5 + labels: + - "dependencies" + commit-message: + prefix: "deps" + # One grouped PR for routine minor/patch bumps instead of one PR per package. + groups: + python-minor-patch: + update-types: + - "minor" + - "patch" + # Heavy, tightly version-pinned core libs: keep these as individual PRs so a + # major bump (e.g. torch, transformers) is reviewed in isolation. + ignore: + - dependency-name: "torch" + update-types: ["version-update:semver-major"] + - dependency-name: "sentence-transformers" + update-types: ["version-update:semver-major"] + + # GitHub Actions used in .github/workflows/* + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + target-branch: "dev-claude" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "github-actions" + commit-message: + prefix: "ci" + groups: + actions-all: + patterns: + - "*" diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 94562c6..971230b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -14,9 +14,9 @@ jobs: package: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python 3.12 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.12" - name: Install build dependencies diff --git a/.github/workflows/claude-implement.yaml b/.github/workflows/claude-implement.yaml new file mode 100644 index 0000000..4257f1d --- /dev/null +++ b/.github/workflows/claude-implement.yaml @@ -0,0 +1,61 @@ +name: Claude Code Implement + +on: + issue_comment: + types: [created] + +permissions: + contents: write + pull-requests: write + issues: write + actions: read + id-token: write + +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number }}-${{ github.actor }} + cancel-in-progress: true + +jobs: + implement: + # Trigger on @claude mentions in issue comments — only by repo owner, ignore bots + if: > + github.actor == 'mengerj' && + github.event.sender.type != 'Bot' && + !github.event.issue.pull_request && + contains(github.event.comment.body, '@claude') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: dev-claude + fetch-depth: 0 + - uses: anthropics/claude-code-action@v1 + id: claude + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + show_full_output: true + base_branch: dev-claude + additional_permissions: | + actions: read + claude_args: | + --allowedTools "Bash,Read,Write,Edit,Glob,Grep,Task,WebSearch,WebFetch" + --max-turns 30 + --output-format stream-json + # The CLAUDE.md in the repo root provides the plan-first protocol, + # branch naming conventions, and PR linking instructions. + # Claude reads it automatically on checkout. + + - name: Archive Claude session log + if: always() + run: | + mkdir -p claude-logs + echo '${{ steps.claude.outputs.result }}' > claude-logs/session.json + shell: bash + + - name: Upload session log + if: always() + uses: actions/upload-artifact@v7 + with: + name: claude-implement-issue${{ github.event.issue.number }}-${{ github.run_number }} + path: claude-logs/ + retention-days: 30 diff --git a/.github/workflows/claude-review.yaml b/.github/workflows/claude-review.yaml new file mode 100644 index 0000000..92a1069 --- /dev/null +++ b/.github/workflows/claude-review.yaml @@ -0,0 +1,56 @@ +name: Claude Code Review + +on: + issue_comment: + types: [created] + +permissions: + contents: write + pull-requests: write + issues: write + actions: read + id-token: write + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.issue.number }}-${{ github.actor }} + cancel-in-progress: true + +jobs: + review: + # Respond to @claude in PR comments (owner only, ignore bots) + if: > + github.event.sender.type != 'Bot' && + github.actor == 'mengerj' && + github.event.issue.pull_request && + contains(github.event.comment.body, '@claude') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - uses: anthropics/claude-code-action@v1 + id: claude + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + show_full_output: true + additional_permissions: | + actions: read + claude_args: | + --allowedTools "Bash,Read,Write,Edit,Glob,Grep,Task" + --max-turns 30 + --output-format stream-json + + - name: Archive Claude session log + if: always() + run: | + mkdir -p claude-logs + echo '${{ steps.claude.outputs.result }}' > claude-logs/session.json + shell: bash + + - name: Upload session log + if: always() + uses: actions/upload-artifact@v7 + with: + name: claude-review-pr${{ github.event.pull_request.number || github.event.issue.number }}-${{ github.run_number }} + path: claude-logs/ + retention-days: 30 diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml new file mode 100644 index 0000000..d24c6de --- /dev/null +++ b/.github/workflows/codeql.yaml @@ -0,0 +1,43 @@ +name: CodeQL + +# Semantic code scanning for Python. Findings appear under the repo's +# Security -> Code scanning tab and as PR annotations. +# Docs: https://docs.github.com/code-security/code-scanning + +on: + push: + branches: [main, dev-claude] + pull_request: + branches: [main, dev-claude] + schedule: + # Weekly full scan (Mondays 06:00 UTC) to catch newly-disclosed query updates. + - cron: "0 6 * * 1" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: Analyze (python) + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: python + # security-and-quality adds maintainability/quality queries on top of + # the default security set. Drop to "security-extended" if too noisy. + queries: security-and-quality + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:python" diff --git a/.github/workflows/dependabot-auto-merge.yaml b/.github/workflows/dependabot-auto-merge.yaml new file mode 100644 index 0000000..07e1872 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yaml @@ -0,0 +1,36 @@ +name: Dependabot auto-merge + +# Auto-approves and enables auto-merge for low-risk Dependabot PRs (patch/minor, +# including security updates). GitHub still waits for all required status checks +# (tests, CodeQL) to pass before the merge actually happens. +# +# Prerequisites (one-time, in repo Settings): +# - "Allow auto-merge" enabled (Settings -> General -> Pull Requests) +# - Branch protection on dev-claude requiring the Test + CodeQL checks +# Docs: https://docs.github.com/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions + +on: pull_request + +permissions: + contents: write + pull-requests: write + +jobs: + auto-merge: + runs-on: ubuntu-latest + if: github.actor == 'dependabot[bot]' + steps: + - name: Fetch Dependabot metadata + id: meta + uses: dependabot/fetch-metadata@v3 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Enable auto-merge for patch/minor updates + if: steps.meta.outputs.update-type == 'version-update:semver-patch' || steps.meta.outputs.update-type == 'version-update:semver-minor' + run: | + gh pr review --approve "$PR_URL" + gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d0a1a6d..0473e17 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -15,11 +15,11 @@ jobs: permissions: id-token: write # IMPORTANT: this permission is mandatory for trusted publishing steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: filter: blob:none fetch-depth: 0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.x" cache: "pip" diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8e23d94..2d025ae 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -2,9 +2,9 @@ name: Test on: push: - branches: [main] + branches: [main, dev-claude] pull_request: - branches: [main] + branches: [main, dev-claude] schedule: - cron: "0 5 1,15 * *" @@ -41,7 +41,7 @@ jobs: PYTHON: ${{ matrix.python }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Free up disk space run: | @@ -52,7 +52,7 @@ jobs: sudo rm -rf /usr/local/lib/android - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} @@ -77,4 +77,4 @@ jobs: run: | coverage report - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v7 diff --git a/.gitignore b/.gitignore index da806f3..343bb0b 100644 --- a/.gitignore +++ b/.gitignore @@ -112,3 +112,4 @@ modules/.calmate/cache/ cache/ mmcontext_out/ +tutorials/outputs/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 81057ec..3785c34 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,11 +11,11 @@ repos: hooks: - id: prettier - repo: https://github.com/tox-dev/pyproject-fmt - rev: "v2.5.0" + rev: "v2.24.1" hooks: - id: pyproject-fmt - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.3 + rev: v0.15.17 hooks: - id: ruff types_or: [python, pyi, jupyter] @@ -23,7 +23,7 @@ repos: - id: ruff-format types_or: [python, pyi, jupyter] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: detect-private-key - id: check-ast diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7dbfc68 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,93 @@ +# CLAUDE.md — mmcontext + +## Project Overview + +mmcontext is a Python library for multimodal contrastive learning that aligns text and omics (single-cell gene expression) embeddings using the sentence-transformers (>=5.4) framework. It enables joint embedding spaces where text descriptions and biological data can be compared directly. + +**Repository**: `github.com/mengerj/mmcontext` +**Maintainer**: Jonatan Menger +**Python**: >=3.11, <3.14 +**Key dependency**: `sentence-transformers>=5.4` (multimodal API) + +## Package Structure + +``` +src/mmcontext/ +├── modules/ # sentence-transformers InputModules +│ ├── mmcontext_module.py # Core: text tokenization + omics vector pass-through +│ ├── adapter_module.py # Projects omics vectors into shared embedding space +│ └── omics_attention_module.py # Optional self-attention for omics tokens +├── embed/ # Embedding pipeline, dataset utilities, model utils +├── eval/ # Evaluation: kNN, ARI, scIB metrics, label similarity +├── io/ # VectorStore, data preparation +├── pl/ # Plotting utilities +├── _legacy/ # Archived modules (do not modify unless removing) +├── callback.py # Training callbacks +├── hub_utils.py # HuggingFace Hub integration +├── simulator.py # Synthetic data generation for testing +└── utils.py # Shared utilities +``` + +## Code Style + +- **Formatter/linter**: ruff (line-length=120) +- **Docstrings**: numpy-style, required for public classes and functions (D100/D104/D105/D107 ignored) +- **Imports**: sorted by isort (via ruff) +- **Type hints**: use them for all public function signatures +- **Pre-commit**: prettier, pyproject-fmt, ruff, detect-private-key, check-ast + +## Testing Conventions + +- Tests live in `tests/` at repo root +- Use pytest fixtures; shared fixtures go in `conftest.py` +- Test files: `test_.py` +- The `simulator.py` module generates synthetic data for tests — prefer it over loading real datasets +- Tests must pass on Python 3.11, 3.12, 3.13 + +## PR Conventions + +- PRs target `dev-claude` (not `main`) unless it's a release +- PR description must reference the issue: `Fixes #` or `Closes #` +- All CI checks must pass before merge +- Keep PRs focused — one logical change per PR + +## Issue Implementation Protocol + +When implementing a feature from a GitHub issue (via @claude or otherwise): + +1. **If the issue is ambiguous**: Post clarifying questions as a comment. Do NOT start implementation until the questions are answered. + +2. **Plan first**: Before writing any code, post an implementation plan as a comment on the issue with a checkbox list: + + ``` + ## Implementation Plan + - [ ] Step 1: description + - [ ] Step 2: description + - [ ] Step 3: description + - [ ] Verify: run tests, check linting + ``` + + Wait for approval (a reply containing "approved", "go ahead", "LGTM", or "looks good"). + +3. **Implement**: Create a branch `claude/-` from `dev-claude`. Implement the plan step by step. Edit the plan comment to check off completed steps. + +4. **Open PR**: Create a PR targeting `dev-claude` with `Fixes #` in the description. Include a summary of what was done and any decisions made. + +## Architecture Notes + +The core pipeline follows the sentence-transformers module pattern: + +``` +Input → MMContextModule → AdapterModule → [OmicsAttentionModule] → Pooling → Loss +``` + +- **MMContextModule**: Handles both text (tokenize → AutoModel) and omics (VectorStore lookup or direct input) modalities. Outputs a unified features dict with `token_embeddings`, `attention_mask`, and `modality_ids`. +- **AdapterModule**: Projects omics vectors into the text model's embedding space. +- **OmicsAttentionModule**: Optional self-attention layer for omics tokens. +- Data stored in anndata format (`.h5ad`), converted via `adata-hf-datasets` for HuggingFace compatibility. + +## What NOT to Change + +- Do not modify files in `_legacy/` unless explicitly removing them +- Do not change the sentence-transformers module interface contracts (features dict keys) +- Do not add dependencies without discussion — the package already has heavy deps (torch, transformers, scanpy) diff --git a/MIGRATION_PLAN.md b/MIGRATION_PLAN.md new file mode 100644 index 0000000..d3f9d25 --- /dev/null +++ b/MIGRATION_PLAN.md @@ -0,0 +1,282 @@ +# Migration Plan: Split evaluation/benchmarking into `mmcontext-benchmark` + +**Status:** in progress — benchmark pipeline implemented (see §0); `mmcontext` +slim-down (§3–§5) still pending. +**Author:** Jonatan Menger (with Claude) +**Date:** 2026-06-03 (updated 2026-06-09) +**Goal:** Make `mmcontext` a focused model package (architecture + lightweight, +single-model evaluation runnable from a notebook). Move all multi-model +comparison, competitor integrations (SCSA, CellWhisperer), and the heavy +embedding/eval orchestration into a new sibling repo, `mmcontext-benchmark`. +Drop the `adata-hf-datasets` package dependency from `mmcontext`. + +--- + +## 0. Implemented benchmark design (2026-06-09) + +The benchmark repo was built around an **adapter + artifact-contract** +architecture rather than a straight file-move of the legacy orchestrators. What +landed (and where it diverged from the original §2–§3 plan): + +- **Artifact contract** (`mmcontext_benchmark/artifacts.py`): one on-disk layout + per `(dataset, model)` — embedders write `embeddings.parquet` + `subset.h5ad` + - `*_label_embeddings_*`; direct classifiers write + `predictions_