Contributing to vibe-qc

Contributions and feedback both welcome. The decision tree below covers where to direct what.

This repository owns the engine, vibe-basis/, and the shared marketing site. Report viewer work in vibe-view, scheduling work in vibe-queue, and format/specification work in QVF. Each companion owns its code, documentation and releases; a core patch does not update a companion checkout. Public-site ownership and shared CSS are covered in site publishing.

Where to report what

What

Where

Security vulnerability — exploitable bug, memory corruption, anything you think shouldn’t be public-by-default

Email mpei@vibe-qc.com directly. PGP fingerprint CC6D 30BB DF96 F694 C615  FBDE 4CD5 65CF 26B1 E7E5 (key at https://vibe-qc.com/docs/_static/pgp/mpei.asc). See SECURITY.md for what to include and the disclosure timeline. Do not open a public issue.

Bug, install problem, missing feature, “this didn’t work the way the docs said”

GitHub issues. Search for an existing issue first; comment on it if there is one. Filing requires a GitHub account.

General feedback, question, “is this a bug or am I doing it wrong?”

Email mpei@vibe-qc.com. No GitLab account needed; replies go to your address directly.

Code-of-conduct violation

Email mpei@vibe-qc.com. See CODE_OF_CONDUCT.md.

You want to send a patch

Read the rest of this file.

You fixed something, or you want to check someone else’s fix

The issue it belongs to. Verification requests and verdicts are comments on the corresponding issue, never files in the repository — see How a fix gets verified.

You want to fund the project

GitHub Sponsors (recurring monthly) or Ko-fi (one-time). Full pitch + author bio + funding goals on the support page.

All contributors and maintainers are expected to follow the Code of Conduct.

How a fix gets verified

Every fix to a wrong answer, a numerical result, or anything release-blocking is checked by someone other than its author before the issue closes. That is a project rule, not a formality: the author of a fix is the person least able to see what it missed.

The request and the verdict are comments on the issue. When your fix lands, comment on the issue it fixes with what a second person needs in order to attack it:

  • the claim, in one paragraph — what was wrong and why the fix is correct;

  • the commit it landed as;

  • the exact commands to run, copy-pasteable;

  • the numbers you measured before the fix, so a verifier can reproduce the broken behaviour on the parent commit and confirm the diagnosis;

  • any test failure that is present but unrelated, named, with why you believe it is unrelated;

  • what you would attack if you were checking someone else’s version of this — the assumption you are least sure of.

To check someone else’s fix, say so in a comment on the issue, do the work, and comment the verdict with the evidence you measured. Disagreeing with the fix is a useful outcome, not a failed one.

Do not put any of this in a file in the repository. Contributors work from different machines and not all of them can push, so a request written as a file reaches its reader but their answer cannot come back. A comment needs only an account. Maintainers retain private verification and coordination records outside product repositories. Until automatic issue routing is active, maintainers explicitly link a public report to its GitLab issue. Public comments do not imply that the private agent queue has already received the report.

Human contributors may add verification comments after landing. An automated contributor posts the verification brief after its push succeeds and before completing the landing step, because that step closes the editing claim used to attach the brief.

Tests for optional dependencies must run in every environment

Force the branch you want to test; do not wait for an environment that happens to lack the dependency.

# Good: exercises the missing-dependency path whether or not trexio is
# installed, so it runs in every venv and in CI.
monkeypatch.setitem(sys.modules, "trexio", None)
with pytest.raises(ImportError) as excinfo:
    trexio_format._require_trexio()

The shape to avoid is a test gated to run only when a dependency is absent, typically @pytest.mark.skipif(HAVE_SOMETHING, ...). A broken assertion inside such a gate is invisible to everyone who has the package installed, and it can stay broken indefinitely because the people who could see the failure are the ones not running it. A test that runs only in the environment you do not have is a test nobody reads the failures of.

That is not hypothetical: vibe-view carried two assertions inside a skipif(HAVE_RDKIT) gate that passed in CI only because the checkout directory happened to be named vibe-view, so a substring check against a path-derived install hint matched by coincidence. They were asserting where the repository sat on disk, not that the message helped a user. Found and fixed 2026-09-10.

This suite currently has no such gate, and it is worth keeping that way. To check:

grep -rnE 'skipif\(\s*(HAVE_|HAS_|_HAS_|has_)[A-Za-z_0-9]*\s*[,)]' tests/

Read the hits rather than counting them, and check the direction before believing either result. Only a condition that is true when the dependency is present hides anything:

Condition

Test runs when

Verdict

skipif(HAVE_RDKIT, ...)

RDKit absent

the trap

skipif(not HAVE_RDKIT, ...)

RDKit present

fine, the ordinary needs-the-extra gate

skipif(shutil.which("git") is None, ...)

git present

fine, the safe inverse

A non-empty result is therefore not by itself a finding.

The narrow pattern above already excludes both safe forms, so running just it usually needs no triage at all. Widen it when you want confidence that no variant slipped past, and triage what the wider net drags in. Quote which pattern produced a count, because the two do not agree: sweeping vibe-view’s suite, the documented pattern returned 1 and a widened one returned 7 — six of them the safe which form, and the odd one out, present in both, merely prose inside a docstring rather than a gate at all.

An empty result is also worth nothing until the pattern is shown capable of matching. Plant the shapes in a throwaway file first, confirm the grep finds them, then trust the sweep:

printf 'import pytest\n@pytest.mark.skipif(HAVE_RDKIT)\ndef test_x(): ...\n' \
    > /tmp/gate_control.py
grep -rnE 'skipif\(\s*(HAVE_|HAS_|_HAS_|has_)[A-Za-z_0-9]*\s*[,)]' /tmp/gate_control.py

Related but distinct: it is fine for an extra leg of a test to skip when a companion is absent, as the writer-to-viewer integration above does, provided the core assertions still run unconditionally.

Branch and checkout policy

Clone the public vibe-qc/vibe-qc repository to prepare contributions. Submit patches as pull requests, with the evidence described below; maintainers route accepted changes into canonical GitLab development. Automatic routing is not yet active; see publication status.

Maintainers working in an authorized upstream clone land changes on main with local checks for the affected area. Rebase before pushing:

git pull --rebase origin main
git push origin main

Never force-push main, release, or release tags, and never amend a commit that has already been pushed. When a rebase conflicts in a shared historical design record or release record, preserve both contributors’ facts and review the merged result. Keep ordinary release notes in separate fragments as described below. The agentic loop alone cuts tags and fast-forwards release from a tagged commit on main; the vibe-qc release chat prepares and proves each release. Website changes publish from main after their CI gate; core docs publish from release. See release process and site publishing.

Use one clone per agent task so its branch, index, environment and generated files are isolated. Do not reset or delete another task’s changes. If an existing worktree must be used, check its hook configuration as described below.

Release notes

For each user-visible change, add one Markdown file under changelog.d/, named after the issue or a short descriptive slug. Start it with a ### heading or a - bullet. Tests-only and internal changes may omit a fragment. Do not edit CHANGELOG.md directly for ordinary contributions. Separate fragments let concurrent changes land without conflicting at [Unreleased].

Validate the fragments before committing:

python scripts/assemble_changelog.py --check

The release owner assembles them into [Unreleased] with scripts/assemble_changelog.py when preparing a release; the command and cleanup workflow are in changelog.d/README.md. Released sections remain pinned. Deliberate amendments follow the pre-push guard procedure below.

Personal information

Do not commit personal home-directory paths or unrelated employer information. Use ~/, /home/USER/, or <vibe-qc-checkout> placeholders in examples. The pre-commit hook checks full staged files for private emails, internal host suffixes, private IPs, personal paths and external private terms. Its narrow runner, root, and user allowlist exists for CI accounts and generic fixtures; it is not permission to add personal data under another name. The hook and CI use the shared .githooks/source_privacy.py implementation.

Documentation prose

Use commas, parentheses or ordinary hyphens instead of em or en dashes in Markdown prose under docs/. The hook checks each touched file’s full staged content, including older prose. Fenced code blocks, inline code and inline math are exempt because they may contain literal output or mathematical notation. Provenance-marked QVF snapshots under docs/qvf/_vendored/ are also exempt: change those in the QVF source repository, then deliberately re-vendor and update the pin.

These public policies are the source of truth for the hooks. Do not bypass hooks unless the maintainer explicitly authorizes the particular exception; record that authorization and the reason in the commit message. Privacy failures must be fixed in source or resolved with an exact public-content review; never bypass them with --no-verify.

Before you open a merge request

  1. Confirm no existing issue already covers the change — if there is one, comment on it so we can coordinate.

  2. Run a bounded reproduction or focused tests for the change. A whole area lane is optional broad validation, not a default pre-push requirement:

    .venv/bin/python scripts/test_gate/run_full_suite.py \
        --wt "$PWD" --py .venv/bin/python --lane <affected-lane>
    
  3. If your change touches documentation, build the Sphinx docs locally and skim the affected pages:

    sphinx-build -b html docs/ docs/_build/html
    
  4. For anything that adds a dependency or changes public API, open an issue first to check scope fit.

Build against the vendored dependencies before trusting a lane

A missing third_party/<dep>/install/ makes the top-level CMake configure fall back to system discovery for that one dependency, and it does so silently. Set VIBEQC_REQUIRE_VENDORED=ON on any build whose test numbers you intend to report, so that fallback becomes a configure-time error instead:

VIBEQC_REQUIRE_VENDORED=ON .venv/bin/pip install -e . \
    --no-build-isolation --config-settings=build-dir=build/<your-build-dir>

What this catches is not a wrong number. A system libint built without the one-body derivative kernels (overlap1, kinetic1, elecpot1) links fine and passes every lane that never differentiates, then calls a null kernel pointer the moment one does: compute_gradient takes SIGSEGV inside an OpenMP worker, which kills the pytest process and takes the rest of that lane’s results with it. Homebrew’s libint 2.13.1 is such a build; the vendored one is not.

This bites hardest in a worktree, which starts with no third_party/ at all, so an agent harness that builds in one gets the fallback by default. See installation for what scripts/setup_native_deps.sh fetches and where it installs.

Pre-commit hook (one-time setup)

After cloning, point git at the tracked .githooks/ directory so the repo’s commit guards run on every commit:

git config --local core.hooksPath .githooks

This single setting activates every hook in .githooks/ (git resolves one hooks directory for all hook types):

  • pre-commit — first refuses a commit whose author or committer email is not a public address (mpei@vibe-qc.com, project@vibe-qc.com, or one you add with git config --local --add privacy.commitEmail you@example.org); set the clone’s identity with git config --local user.email. It then refuses commits whose staged file content contains absolute paths into the author’s home directories or other personal-info patterns (personal information). It then runs .githooks/check_no_em_dashes.py, which refuses staged docs/*.md carrying em or en dashes in prose (code fences, inline code, and inline math are exempt).

  • commit-msg — refuses a release: vX.Y.Z commit unless pyproject.toml [project] version == X.Y.Z and a ## [vX.Y.Z] CHANGELOG header exists. This is the backstop against tagging a release from a tree whose version bump was skipped — the defect that shipped v0.11.2 / v0.11.3 self-reporting 0.11.1. See maintainer publication.

  • pre-push — refuses a push to main or release-candidate/* whose CHANGELOG.md changes an already released section without re-pinning it in scripts/test_gate/changelog_pins.toml (#229). It runs at push time because misfiled entries arrive through rebases, which never run pre-commit. Ordinary unreleased notes belong in changelog.d/ until release assembly; a deliberate amendment to a released section is re-pinned with python scripts/test_gate/changelog_guard.py pin vX.Y.Z --amended "why". The committed guard, pins and changelog must all be readable regular files; repairing only the working tree does not repair the pushed commit (#264). Historical commits are exempt only when neither policy file occurs in their complete reachable history. Deleting the guard or pins does not restore that exemption. If shallow history prevents proving it, fetch complete history before retrying. Deletions and pushes to unrelated destination refs remain exempt. The hook uses the committed guard as trusted project code; it is not a substitute for reviewing changes to the guard itself.

Fix hook failures before committing. A maintainer-authorized exception follows the documentation and hook policy above.

Worktrees need the setting applied per worktree

Parallel agent tasks get one clone each, following the branch and checkout policy. Worktrees still turn up in practice (agent harnesses create them under .claude/worktrees/), and a worktree does not necessarily inherit the setting above, so check before trusting it.

--local writes the shared .git/config, but once a repo has extensions.worktreeConfig = true — git enables it the first time anything writes a worktree-scoped setting — a linked worktree can carry its own core.hooksPath in .git/worktrees/<name>/config.worktree that overrides the shared value. Check what a worktree actually resolves, and repair it with --worktree:

git -C <worktree> config --show-scope --get-all core.hooksPath
git -C <worktree> config --worktree core.hooksPath .githooks

Keep the path relative. An absolute path bakes the checkout location into each worktree’s config, so renaming or moving the clone leaves core.hooksPath pointing at a directory that no longer exists. Git skips a missing hooks directory silently — no warning, no error, no failed commit — so every guard above goes inert without anyone noticing. A relative .githooks resolves against each worktree’s own top level and survives the move.

To confirm a hook really runs, without making a commit:

git -C <worktree> hook run pre-commit

AI-assisted contributions

AI-assisted patches follow the same contributor, licensing and verification policies established for this project, including the policy work tracked in #195.

Disclose AI assistance in the pull request or merge request, including which parts it helped produce. The contributor remains responsible for understanding and reviewing the patch, its tests, citations and dependency choices. Check referenced sources directly and remove generated claims that cannot be verified.

Include tests that exercise the reported failure and the corrected behavior. For numerical changes, provide the relevant before/after values, reference settings, tolerances and validation comparisons. Identify the exact commit and build that were tested, and state missing checks or unrelated failures.

GitHub contributions are admitted only through a GitLab merge request when public contribution intake is enabled. A maintainer must not merge an imported patch without green GitLab CI for its current head. A changed head requires new validation; skipped or stale pipelines do not qualify. The independent review rules above still apply to numerical and release-blocking changes. The Licensing section below governs these contributions without changes to its terms.

Code style

  • C++17. Four-space indent, brace on same line for control flow. Everything in cpp/ lives under namespace vibeqc { ... }. Match the patterns in cpp/src/rhf.cpp / cpp/src/integrals.cpp.

  • Python 3.11+. PEP 8 with four-space indent. Start every module with from __future__ import annotations. Type hints on public API; local helpers can skip them.

  • Prefer editing existing modules over introducing new ones. If a new file is the right choice, follow the neighbors’ layout.

Commit messages

  • Imperative mood, first line under 72 characters (e.g. Fix SCF divergence on CH3 with SAD guess).

  • Longer rationale in the body if the what doesn’t explain itself.

  • Co-author trailers are fine for pair work.

  • To flag a commit for inclusion in a patch release, add a Patch-candidate: trailer to the commit body (alongside Signed-off-by: / Co-Authored-By:) — values like v0.7.x, v0.8.x, v0.8.0, or comma-separated combinations. The release chat scans these when preparing patches. Do not tag vX.Y.Z, push to release, or open MRs against release directly — the agentic loop performs those, and (since 2026-05-15) GitLab branch / tag protection blocks them. See branch and checkout policy and docs/release_process.md for the full convention.

What we won’t accept (for now)

  • API changes that break public signatures without a deprecation path.

  • New hard dependencies added without prior discussion — open an issue first.

  • Changes that regress the test suite without a stated rationale and a plan to restore.

Licensing

By submitting a patch, pull request, or any other contribution to vibe-qc, you agree that:

  1. Your contribution is licensed under the Mozilla Public License 2.0 (the project license — see LICENSE).

  2. You grant the project owner (Michael F. Peintinger) the right to relicense your contribution under alternative terms, including a future commercial license, alongside the MPL 2.0 public license. You retain copyright.

This is a lightweight alternative to a formal Contributor License Agreement. If you’re not comfortable with (2), please open an issue before contributing so we can discuss.

vibe-qc’s compiled core links libint (MPL 2.0), libxc (MPL 2.0), Eigen (MPL 2.0 / BSD-3), spglib (BSD-3), and pybind11 (BSD-3) — all MPL-compatible.

Private privacy policy

The portable guard checks privacy patterns without storing real private names. Operators can add private literal terms through VIBE_PRIVACY_TERMS_FILE or the clone-local privacy.termsFile Git setting. Use an absolute path to a UTF-8 file outside the source checkout, with one literal term per line. Matching is case-insensitive; a configured missing, empty or in-tree file blocks the check. Keep the private terms file and resolved installation paths out of commits. The repository’s privacy tests load the same external policy when configured.

Site wrappers and deployment configuration are maintained separately in private operations storage. Product changes must use portable examples and preserve the existing generic installation and configuration APIs.

Fast source privacy preflight

Enable git config --local core.hooksPath .githooks once in each clone. Run python3 .githooks/source_privacy.py --staged before committing and python3 .githooks/source_privacy.py --tree HEAD before requesting a release candidate build. The same current-tree check runs in the first CI stage on ordinary main pushes and release candidates, without building the numerical core.

All tracked text is covered, including instructions and hooks. Keep operator logs, machine details and private configuration outside product checkouts. Use VIBE_PRIVACY_TERMS_FILE or local Git privacy.termsFile for an external UTF-8 file with one private literal per line. A configured empty, missing or in-tree file blocks the check. Canonical CI supplies this file from private CI configuration; the public repository contains no private term list.

The two public maintainer addresses and reserved documentation domains are allowed. Public upstream attribution or syntax that resembles a private value requires independent, exact-context review in .githooks/public_content_review.json. Do not approve private values or generate a blanket baseline. Changing the text invalidates its exception. Binary assets and historical public objects remain covered by the automatic publication gate; this fast check does not replace it.