From 90628217f28006ed3379c7b29a7574f3f4a2ed22 Mon Sep 17 00:00:00 2001 From: Jayson Steffens Date: Mon, 20 Apr 2026 11:00:51 +0200 Subject: [PATCH 1/4] chore: fix ruff lint errors Remove 6 unused locals and 1 ambiguous variable name to get ruff passing with zero warnings. All 151 tests still pass. --- snapvec/_fast_fallback.py | 1 - snapvec/_ivfpq.py | 1 - snapvec/_kmeans.py | 1 - snapvec/_pq.py | 1 - snapvec/_residual.py | 1 - tests/test_ivfpq.py | 3 --- tests/test_ivfpq_filter.py | 10 +++++----- tests/test_residual.py | 1 - 8 files changed, 5 insertions(+), 14 deletions(-) diff --git a/snapvec/_fast_fallback.py b/snapvec/_fast_fallback.py index 240e60e..7f9b895 100644 --- a/snapvec/_fast_fallback.py +++ b/snapvec/_fast_fallback.py @@ -25,7 +25,6 @@ def fused_gather_adc( parallel: bool = True, ) -> None: M = all_codes.shape[0] - n = len(row_idx) cat = all_codes[:, row_idx] scores[:] = coarse_offsets for j in range(M): diff --git a/snapvec/_ivfpq.py b/snapvec/_ivfpq.py index 11dea81..e1d4538 100644 --- a/snapvec/_ivfpq.py +++ b/snapvec/_ivfpq.py @@ -29,7 +29,6 @@ """ from __future__ import annotations -import os import struct import threading import warnings diff --git a/snapvec/_kmeans.py b/snapvec/_kmeans.py index 5e935b2..56d1c81 100644 --- a/snapvec/_kmeans.py +++ b/snapvec/_kmeans.py @@ -10,7 +10,6 @@ """ from __future__ import annotations -from typing import Any import numpy as np from numpy.typing import NDArray diff --git a/snapvec/_pq.py b/snapvec/_pq.py index e6789c2..a725e91 100644 --- a/snapvec/_pq.py +++ b/snapvec/_pq.py @@ -25,7 +25,6 @@ """ from __future__ import annotations -import os import struct from pathlib import Path from typing import Any diff --git a/snapvec/_residual.py b/snapvec/_residual.py index 6976ee7..a8365ca 100644 --- a/snapvec/_residual.py +++ b/snapvec/_residual.py @@ -23,7 +23,6 @@ """ from __future__ import annotations -import os import struct from pathlib import Path from typing import Any diff --git a/tests/test_ivfpq.py b/tests/test_ivfpq.py index 45c6ff0..8521e65 100644 --- a/tests/test_ivfpq.py +++ b/tests/test_ivfpq.py @@ -137,7 +137,6 @@ def test_search_batch_unnormalized_matches_loop() -> None: in) must still match the per-query loop, since the multi-step pipeline (norm, RHT-or-not, gather, norm-multiply) is more sensitive than the normalized=True path.""" - rng = np.random.default_rng(95) d, n_corpus, n_queries = 64, 500, 20 base = _clustered(n_corpus, d, n_clusters=20, seed=95) scales = np.linspace(0.5, 5.0, n_corpus).astype(np.float32) @@ -285,7 +284,6 @@ def test_rerank_candidate_pool_respects_norms_when_unnormalized() -> None: high-norm vector that genuinely wins on ⟨q, v⟩ can miss the candidate pool because its unit-sphere PQ score is modest. This was the high-priority bug caught on the PR #27 review.""" - rng = np.random.default_rng(140) # Build corpus where recall truth depends heavily on per-vector # norm magnitude: same directions scaled across a wide range. d, n_corpus = 64, 400 @@ -611,7 +609,6 @@ def test_load_validates_offsets(tmp_path: Path) -> None: idx.save(path) # Corrupt: scramble offsets to be non-monotone (swap first two). - buf = bytearray(path.read_bytes()) # Offsets are stored right after coarse + codebooks. Easier to # mutate via a fresh reload, edit, re-save. reloaded = IVFPQSnapIndex.load(path) diff --git a/tests/test_ivfpq_filter.py b/tests/test_ivfpq_filter.py index 908c4f5..773e47f 100644 --- a/tests/test_ivfpq_filter.py +++ b/tests/test_ivfpq_filter.py @@ -157,15 +157,15 @@ def test_search_batch_filter_matches_loop_of_search() -> None: ] assert len(batch_hits) == len(loop_hits) - for b, l in zip(batch_hits, loop_hits): + for b, loop in zip(batch_hits, loop_hits): # Same id count, every hit in filter, and the returned score # for the *worst* hit in each list is within fp tolerance of - # the other's worst — proves both paths converge at the same + # the other's worst -- proves both paths converge at the same # top-k boundary even if tied candidates reorder inside it. - assert len(b) == len(l) + assert len(b) == len(loop) assert all(h[0] in filter_ids for h in b) - assert all(h[0] in filter_ids for h in l) - assert abs(b[-1][1] - l[-1][1]) < 1e-4 + assert all(h[0] in filter_ids for h in loop) + assert abs(b[-1][1] - loop[-1][1]) < 1e-4 def test_search_batch_filter_all_unknown_returns_empties() -> None: diff --git a/tests/test_residual.py b/tests/test_residual.py index 4d97440..7ad91fd 100644 --- a/tests/test_residual.py +++ b/tests/test_residual.py @@ -30,7 +30,6 @@ def test_basic_build_search() -> None: def test_rerank_matches_full() -> None: - rng = np.random.default_rng(2) corpus = _unit_gaussian(800, 128, seed=2) queries = _unit_gaussian(20, 128, seed=3) idx = ResidualSnapIndex(dim=128, b1=3, b2=3, seed=0, normalized=True) From 87bed9516dca5f602f44035260d2037a281d1b4a Mon Sep 17 00:00:00 2001 From: Jayson Steffens Date: Mon, 20 Apr 2026 11:01:03 +0200 Subject: [PATCH 2/4] chore: add CI, release pipeline, and community files (phase 1) Foundation for professionalizing the project. - ci.yml: pytest on ubuntu/macOS/windows x py3.10/3.12 (+3.13 on ubuntu), ruff check as required, mypy --strict as warning-only. - release.yml: cibuildwheel for linux-x86_64/aarch64, macOS-x86_64/arm64, windows-AMD64 x py3.10-3.13; PyPI trusted publishing on tag v*. - dependabot: monthly updates for actions and pip dev deps. - Issue templates (bug, feature) + PR template + Discussions link. - SECURITY, CODE_OF_CONDUCT, CONTRIBUTING with full dev setup. - .pre-commit-config.yaml with ruff + file hygiene hooks. - README badges: PyPI, Python versions, CI, license, downloads. - CLAUDE.md with project-specific commit and code style rules. --- .github/ISSUE_TEMPLATE/bug_report.yml | 68 ++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 ++ .github/ISSUE_TEMPLATE/feature_request.yml | 29 ++++++++ .github/PULL_REQUEST_TEMPLATE.md | 20 ++++++ .github/dependabot.yml | 19 +++++ .github/workflows/ci.yml | 74 +++++++++++++++++++ .github/workflows/release.yml | 84 ++++++++++++++++++++++ .pre-commit-config.yaml | 19 +++++ CLAUDE.md | 28 ++++++++ CODE_OF_CONDUCT.md | 31 ++++++++ CONTRIBUTING.md | 73 +++++++++++++++++++ README.md | 6 ++ SECURITY.md | 41 +++++++++++ 13 files changed, 497 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .pre-commit-config.yaml create mode 100644 CLAUDE.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 SECURITY.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..5544437 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,68 @@ +name: Bug report +description: Report a correctness, performance, or crash issue in snapvec. +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to file a report. A minimal reproducer + and your environment details shorten the feedback loop a lot. + + - type: textarea + id: summary + attributes: + label: Summary + description: One or two sentences describing the issue. + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Reproduction + description: Minimal code that triggers the issue. Include random seeds. + render: python + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected vs actual behaviour + validations: + required: true + + - type: input + id: snapvec-version + attributes: + label: snapvec version + placeholder: "0.9.0" + validations: + required: true + + - type: input + id: python-version + attributes: + label: Python version + placeholder: "3.12.2" + validations: + required: true + + - type: input + id: numpy-version + attributes: + label: NumPy version + placeholder: "1.26.4" + + - type: input + id: platform + attributes: + label: OS and CPU + placeholder: "macOS 14.4 arm64 / Ubuntu 22.04 x86_64 / ..." + validations: + required: true + + - type: textarea + id: additional + attributes: + label: Additional context diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..9025142 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Question or usage help + url: https://github.com/stffns/snapvec/discussions + about: For questions about using snapvec, please use GitHub Discussions. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..2dd7e9a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,29 @@ +name: Feature request +description: Propose a new feature or an API change. +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: What problem are you trying to solve? + description: A clear description of the use case or limitation. + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: Proposed solution + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + + - type: textarea + id: additional + attributes: + label: Additional context + description: Links to papers, related projects, benchmarks, or prior art. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..d405238 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,20 @@ +## Summary + + + +## Type of change + +- [ ] Bug fix +- [ ] Feature +- [ ] Performance improvement +- [ ] Refactor +- [ ] Docs / tests only + +## Checklist + +- [ ] Tests added or updated +- [ ] `ruff check` and `ruff format` pass +- [ ] `pytest -q` passes locally +- [ ] CHANGELOG updated under `[Unreleased]` +- [ ] On-disk format version bumped if the format changed +- [ ] Benchmark before/after numbers included (for performance PRs) diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b1a4ccf --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + open-pull-requests-limit: 5 + + - package-ecosystem: pip + directory: / + schedule: + interval: monthly + open-pull-requests-limit: 5 + groups: + dev-dependencies: + patterns: + - "pytest*" + - "mypy*" + - "ruff" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a375b4c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,74 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: Lint (ruff + mypy) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + + - name: Install dev dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + + - name: ruff check + run: ruff check snapvec/ tests/ + + - name: mypy (strict, warning-only for now) + run: mypy --strict snapvec/ || true + + test: + name: Test ${{ matrix.os }} / py${{ matrix.python-version }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-14, windows-latest] + python-version: ["3.10", "3.12"] + include: + - os: ubuntu-latest + python-version: "3.13" + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - name: Install libomp (macOS) + if: runner.os == 'macOS' + run: brew install libomp + + - name: Install package + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + + - name: Run tests + run: pytest -q --cov=snapvec --cov-report=term-missing + + - name: Upload coverage (ubuntu + py3.12 only) + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: .coverage + if-no-files-found: ignore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6c69d2d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,84 @@ +name: Release + +on: + push: + tags: ["v*"] + workflow_dispatch: + +jobs: + build_wheels: + name: Wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + cibw_archs: "x86_64" + - os: ubuntu-24.04-arm + cibw_archs: "aarch64" + - os: macos-13 + cibw_archs: "x86_64" + - os: macos-14 + cibw_archs: "arm64" + - os: windows-latest + cibw_archs: "AMD64" + steps: + - uses: actions/checkout@v4 + + - name: Build wheels + uses: pypa/cibuildwheel@v2.22 + env: + CIBW_BUILD: "cp310-* cp311-* cp312-* cp313-*" + CIBW_SKIP: "*-musllinux_* pp*" + CIBW_ARCHS: ${{ matrix.cibw_archs }} + CIBW_BEFORE_ALL_MACOS: "brew install libomp" + CIBW_ENVIRONMENT_MACOS: >- + MACOSX_DEPLOYMENT_TARGET=${{ matrix.os == 'macos-13' && '13.0' || '14.0' }} + CIBW_TEST_REQUIRES: "pytest" + CIBW_TEST_COMMAND: "pytest {project}/tests -q" + CIBW_TEST_SKIP: "*-macosx_arm64" + + - uses: actions/upload-artifact@v4 + with: + name: wheels-${{ matrix.os }}-${{ matrix.cibw_archs }} + path: ./wheelhouse/*.whl + + build_sdist: + name: Source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Build sdist + run: | + python -m pip install --upgrade pip build + python -m build --sdist + + - uses: actions/upload-artifact@v4 + with: + name: sdist + path: dist/*.tar.gz + + publish: + name: Publish to PyPI + needs: [build_wheels, build_sdist] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + environment: + name: pypi + url: https://pypi.org/project/snapvec/ + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: Publish via trusted publishing + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..2f5aff5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml + - id: check-added-large-files + args: ["--maxkb=500"] + - id: check-merge-conflict + - id: mixed-line-ending + args: ["--fix=lf"] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.4 + hooks: + - id: ruff + args: ["--fix"] diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..aafac4a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,28 @@ +# snapvec -- Claude-specific instructions + +## Commits +- NEVER add `Co-Authored-By` lines to commit messages. +- Use conventional prefixes: `feat`, `fix`, `chore`, `docs`, `bench`, `refactor`, `test`. +- Keep subject under 72 chars. Present-tense imperative. + +## Code style +- Pure ASCII only in source and docs. No em dashes, no smart quotes, no ellipsis character. Use `-`, `--`, `"`, `'`, `...`. +- English everywhere in code, docstrings, commits, and README. + +## Project layout +- `snapvec/` = library (public API in `__init__.py`). +- `tests/` = pytest suite (151 tests as of v0.9.0). +- `experiments/` = rough benchmarks and profiling scripts -- do not treat as first-class code. +- `bench/` (future) = reproducible benchmark suite that runs in CI. + +## Running checks locally +```bash +ruff check snapvec/ tests/ +pytest -q +mypy --strict snapvec/ # 17 errors as of 2026-04-20, warning-only in CI +``` + +## File format invariants +- Any change to the on-disk format (`.snpv`, `.snpq`, `.snpr`, `.snpi`) must bump + the format version in `snapvec/_file_format.py` and add a round-trip test. +- Do not remove or reorder existing serialized fields without a migration path. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..b87168f --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,31 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone. + +## Our Standards + +Examples of behavior that contributes to a positive environment: +- Using welcoming and inclusive language. +- Being respectful of differing viewpoints and experiences. +- Gracefully accepting constructive criticism. +- Focusing on what is best for the community. + +Examples of unacceptable behavior: +- The use of sexualized language or imagery and unwelcome sexual attention. +- Trolling, insulting or derogatory comments, and personal or political attacks. +- Public or private harassment. +- Publishing others' private information without explicit permission. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project maintainer at **stffens@gmail.com**. All +complaints will be reviewed and investigated promptly and fairly. + +## Attribution + +This Code of Conduct is adapted from the +[Contributor Covenant](https://www.contributor-covenant.org), version 2.1. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..bae49ed --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,73 @@ +# Contributing to snapvec + +Thanks for your interest. snapvec is a small, focused library. Contributions +are welcome; please read this before opening a PR so we can keep the feedback +loop short. + +## Getting started + +```bash +git clone https://github.com/stffns/snapvec.git +cd snapvec +python -m venv .venv && source .venv/bin/activate +pip install -e ".[dev]" +pre-commit install +``` + +On macOS you also need OpenMP: + +```bash +brew install libomp +``` + +## Running the test suite + +```bash +pytest -q +``` + +Coverage: + +```bash +pytest --cov=snapvec --cov-report=term-missing +``` + +## Linting and types + +```bash +ruff check snapvec/ tests/ +ruff format snapvec/ tests/ +mypy --strict snapvec/ +``` + +`pre-commit` runs ruff on every commit. + +## Benchmarks + +Reproducible benchmarks live under `experiments/`. If you add or modify one, +include the hardware (`uname -a`, CPU model), NumPy version, and the commit +hash in the output so results are comparable across runs. + +## Pull requests + +- Keep the change focused. One PR, one idea. +- Add or update tests for every behavior change. +- If the change affects performance, include a before/after measurement. +- If the change affects the on-disk file format, bump the format version in + `snapvec/_file_format.py` and add a round-trip test. +- Update `CHANGELOG.md` under the `[Unreleased]` section. + +## Commit style + +- Use present tense imperative ("add X", "fix Y", not "added" / "fixes"). +- Keep the subject line under 72 characters. +- Reference the issue number in the body if applicable. + +## Release process + +Releases are cut by the maintainer: +1. Bump version in `pyproject.toml`. +2. Move `[Unreleased]` entries to a new version heading in `CHANGELOG.md`. +3. Tag `vX.Y.Z` and push. +4. The `Release` workflow builds wheels and publishes to PyPI via + trusted publishing. diff --git a/README.md b/README.md index 5357b78..bd6fb03 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # snapvec +[![PyPI version](https://img.shields.io/pypi/v/snapvec.svg)](https://pypi.org/project/snapvec/) +[![Python versions](https://img.shields.io/pypi/pyversions/snapvec.svg)](https://pypi.org/project/snapvec/) +[![CI](https://github.com/stffns/snapvec/actions/workflows/ci.yml/badge.svg)](https://github.com/stffns/snapvec/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) +[![Downloads](https://static.pepy.tech/badge/snapvec/month)](https://pepy.tech/project/snapvec) + **Fast compressed approximate nearest-neighbor search. NumPy + Cython compiled kernels.** `snapvec` ships **four index types** for embedding vector search, each targeting a different point on the accuracy / storage / latency frontier: diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..68edcda --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ +# Security Policy + +## Supported Versions + +Security fixes are applied to the latest minor release. Older versions receive +fixes only when the upgrade path is non-trivial. + +| Version | Supported | +| ------- | --------- | +| 0.9.x | Yes | +| < 0.9 | No | + +## Reporting a Vulnerability + +If you believe you have found a security vulnerability in snapvec, please +report it privately rather than opening a public issue. + +Preferred channel: email **stffens@gmail.com** with subject line +`[snapvec security] `. + +Please include: +- A description of the issue and its potential impact. +- Steps to reproduce (minimal code sample if possible). +- The snapvec version, Python version, and operating system. + +You should receive an initial acknowledgement within 7 days. After triage, +we will coordinate a fix and a disclosure timeline with you. + +## Scope + +In scope: +- Memory safety issues in Cython kernels (`snapvec/_fast.pyx`). +- Deserialization vulnerabilities in the on-disk index format + (`snapvec/_file_format.py`, `save` / `load`). +- Numerical correctness issues that produce silent data loss. + +Out of scope: +- Resource exhaustion on adversarial inputs (snapvec is a library, not a + service; the caller owns input validation). +- Issues in third-party dependencies (NumPy, Cython) unless snapvec's usage + is the trigger. From fabcd09778a1a1cde16037d4164dfa6d78f30b7b Mon Sep 17 00:00:00 2001 From: Jayson Steffens Date: Mon, 20 Apr 2026 11:09:48 +0200 Subject: [PATCH 3/4] fix(build): use -mcpu=native on ARM to build on macos-14 runners GitHub's macos-14 arm64 runners ship Xcode 15 clang, which rejects -march=native with 'unknown target CPU apple-m1'. Clang on ARM expects -mcpu=native; -march=native is x86-specific. Detect host arch in setup.py and pick the right flag per platform. MSVC gets neither since it has no equivalent knob. --- setup.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6f32bba..9d429d9 100644 --- a/setup.py +++ b/setup.py @@ -32,14 +32,32 @@ def _openmp_flags(): return (["-fopenmp"], ["-fopenmp"]) +def _arch_flags(): + """Host-optimized CPU target flags per platform. + + GCC/clang on x86 accept ``-march=native``. On ARM (Apple Silicon, + Graviton), clang expects ``-mcpu=native`` instead; passing + ``-march=native`` triggers ``unknown target CPU 'apple-m1'`` on the + Xcode-bundled clang that GitHub macos-14 runners ship. MSVC doesn't + have an equivalent knob. + """ + if sys.platform == "win32": + return [] + machine = platform.machine().lower() + if machine in ("arm64", "aarch64"): + return ["-mcpu=native"] + return ["-march=native"] + + compile_flags, link_flags = _openmp_flags() +arch_flags = _arch_flags() extensions = [ Extension( "snapvec._fast", sources=["snapvec/_fast.pyx"], include_dirs=[np.get_include()], - extra_compile_args=["-O3", "-march=native"] + compile_flags, + extra_compile_args=["-O3"] + arch_flags + compile_flags, extra_link_args=link_flags, ), ] From 070154a576b246629f401971c35115f313932076 Mon Sep 17 00:00:00 2001 From: Jayson Steffens Date: Mon, 20 Apr 2026 11:17:53 +0200 Subject: [PATCH 4/4] fix: address PR #43 bot review feedback - Remove 'ruff format' from CONTRIBUTING.md and PR template: conflicts with the decision to skip it (reformats hand-aligned codebooks). Added a one-liner explaining why it is not used. - Drop stale 'swap first two' comment in test_load_validates_offsets; the test sets offsets[0]=99 to break the offsets[0]==0 invariant. - release.yml: pin MACOSX_DEPLOYMENT_TARGET=13.0 for both x86_64 and arm64 wheels so published arm64 wheels install on macOS 13+ (not just macOS 14+). Also drop CIBW_TEST_SKIP so arm64 wheels are exercised on the native arm64 runner instead of going untested. - Soften CLAUDE.md ASCII rule to 'new or edited' text; the existing docstrings use sqrt/arrows/inner-product notation that should not be churned just for style. --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .github/workflows/release.yml | 4 +--- CLAUDE.md | 6 +++++- CONTRIBUTING.md | 5 +++-- tests/test_ivfpq.py | 8 ++++---- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d405238..3d29c53 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -13,7 +13,7 @@ ## Checklist - [ ] Tests added or updated -- [ ] `ruff check` and `ruff format` pass +- [ ] `ruff check` passes - [ ] `pytest -q` passes locally - [ ] CHANGELOG updated under `[Unreleased]` - [ ] On-disk format version bumped if the format changed diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6c69d2d..050ca85 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,11 +33,9 @@ jobs: CIBW_SKIP: "*-musllinux_* pp*" CIBW_ARCHS: ${{ matrix.cibw_archs }} CIBW_BEFORE_ALL_MACOS: "brew install libomp" - CIBW_ENVIRONMENT_MACOS: >- - MACOSX_DEPLOYMENT_TARGET=${{ matrix.os == 'macos-13' && '13.0' || '14.0' }} + CIBW_ENVIRONMENT_MACOS: "MACOSX_DEPLOYMENT_TARGET=13.0" CIBW_TEST_REQUIRES: "pytest" CIBW_TEST_COMMAND: "pytest {project}/tests -q" - CIBW_TEST_SKIP: "*-macosx_arm64" - uses: actions/upload-artifact@v4 with: diff --git a/CLAUDE.md b/CLAUDE.md index aafac4a..ec95748 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,11 @@ - Keep subject under 72 chars. Present-tense imperative. ## Code style -- Pure ASCII only in source and docs. No em dashes, no smart quotes, no ellipsis character. Use `-`, `--`, `"`, `'`, `...`. +- Prefer ASCII in new or edited text (source, docstrings, docs, commits). + No em dashes, no smart quotes, no ellipsis character. Use `-`, `--`, + `"`, `'`, `...`. Existing math notation in docstrings (`sqrt`, arrows, + inner-product brackets) may remain unless you are already rewriting + that block. - English everywhere in code, docstrings, commits, and README. ## Project layout diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bae49ed..ee47a10 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,11 +36,12 @@ pytest --cov=snapvec --cov-report=term-missing ```bash ruff check snapvec/ tests/ -ruff format snapvec/ tests/ mypy --strict snapvec/ ``` -`pre-commit` runs ruff on every commit. +`ruff format` is intentionally not used: it rewrites the hand-aligned +codebook constants in `snapvec/_codebooks.py` into a less readable layout. +`pre-commit` runs `ruff check` on every commit. ## Benchmarks diff --git a/tests/test_ivfpq.py b/tests/test_ivfpq.py index 8521e65..d197dd2 100644 --- a/tests/test_ivfpq.py +++ b/tests/test_ivfpq.py @@ -608,12 +608,12 @@ def test_load_validates_offsets(tmp_path: Path) -> None: path = tmp_path / "x.snpi" idx.save(path) - # Corrupt: scramble offsets to be non-monotone (swap first two). - # Offsets are stored right after coarse + codebooks. Easier to - # mutate via a fresh reload, edit, re-save. + # Corrupt: break the offsets[0] == 0 invariant so the loader rejects + # the file. Easier to reload, mutate in memory, and re-save than to + # patch raw bytes at the right offset inside the binary format. reloaded = IVFPQSnapIndex.load(path) reloaded._offsets = reloaded._offsets.copy() - reloaded._offsets[0] = 99 # break the offsets[0]==0 invariant + reloaded._offsets[0] = 99 reloaded.save(path) with pytest.raises(ValueError, match="offsets"): IVFPQSnapIndex.load(path)