Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/scheduled_tasks.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"sessionId":"443297b7-74b4-4a29-9baf-9d9ad5fe34a7","pid":13572,"procStart":"134326775626595361","acquiredAt":1788318795346}
51 changes: 51 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Contributing to EdgeVector

Contributions are welcome. The bar here is unusual in one specific way:
**every claim must be measured and every property must be enforced by a
test.** A change that makes the code faster needs a number; a change that
touches the query path needs the allocation gate to still pass; a new file
format field needs hostile-input coverage.

## Build and test

```sh
make -C tests run # 6 suites, asserts enabled
make -C tests run-release # same under -DNDEBUG
make -C examples run # the self-checking quickstart
make -C tests bench # benchmarks (pass clustered|random|itq|million)
```

Requires GCC or Clang with C++17 (MSVC is unsupported: the code uses
`__builtin_popcountll`/`__builtin_prefetch`). Cross/emulated runs use the
Makefile knobs `ARCH`, `CXX`, `RUNNER`; `tests/arm64.Dockerfile` reproduces
the ARM validation locally.

## The rules the code holds itself to

- **Zero allocation, zero syscalls, `noexcept` on the query path.** The test
suites replace the global `operator new` and fail on a single allocation in
any search mode. If your change allocates at query time, it will not merge.
- **No aliasing tricks**: bytes cross into wider types via `std::memcpy`
only; on-disk bytes are never `reinterpret_cast` to structs.
- **Every failure is a status value**, never an exception; a failed load
leaves the object empty. The format loaders are fuzzed
(`tests/test_format_fuzz.cpp`) — new format fields need to survive it.
- **Deterministic ordering**: (distance, id) tie-breaks everywhere, so
results are reproducible and brute-force-comparable.
- **`-Wall -Wextra -Werror` everywhere**, including under sanitizers.
- Kernel optimizations must be **bit-identical** to a naive reference and
gated by an equivalence test.

## CI

Every PR runs seven jobs: Linux gcc + clang, native arm64 (with an
on-silicon benchmark), Windows MinGW, the quickstart via make + CMake,
ASan+UBSan, and ThreadSanitizer on the concurrency suite. Green CI is
necessary but not sufficient — a reviewer will also ask what you measured.

## PRs

Branch from `main`, keep commits explanatory (this repo's history reads as
an engineering narrative — including negative results; a falsified
hypothesis documented honestly is a welcome commit message), and update the
README's numbers only with fresh measurements, stating the hardware.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ to link.
include(FetchContent)
FetchContent_Declare(edgevector
GIT_REPOSITORY https://github.com/JonathanKash/EdgeVector.git
GIT_TAG main) # or pin a commit
GIT_TAG v0.8.0) # or a commit hash, or main for latest
FetchContent_MakeAvailable(edgevector)
target_link_libraries(your_app PRIVATE edgevector::edgevector)
```
Expand Down Expand Up @@ -373,7 +373,7 @@ supported). Linux, WSL, or MinGW-w64 on Windows.

```sh
cd tests
make run # 5 test suites, asserts enabled
make run # 6 test suites, asserts enabled
make run-release # same suites under -DNDEBUG
make bench # the benchmarks reported above (-DNDEBUG; pass a scenario
# to the binary: ./benchmark_100k clustered|random|itq)
Expand All @@ -389,7 +389,10 @@ against float32 ground truth, graph persistence round-trips (bitwise-identical
results and surviving tombstones after reload), context isolation plus a
4-thread concurrency test, delete/restore/filter composition, slot
reclamation (new-vector serving, no-dangling-edge integrity after churn of
100 slots, full entry-point turnover, single-node bootstrap), growable
100 slots, full entry-point turnover, single-node bootstrap), deterministic
fuzzing of all three format loaders (6,000 mutated/truncated/extended files
per run, under ASan in CI: rejected files must leave objects empty, accepted
files must satisfy full structural invariants), growable
capacity (2x growth with a relocated block, parallel fill of the new region,
stale-context invalidation, persistence at the new capacity), ITQ invariants
(orthogonality, exact cosine preservation, monotone objective, determinism,
Expand Down
21 changes: 17 additions & 4 deletions tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,20 @@ ITQ_HEADERS := ../include/edgevector/itq_rotation.hpp \
../include/edgevector/hnsw_graph.hpp \
../include/edgevector/quantize_math.hpp

FUZZ_TARGET := test_format_fuzz$(EXE)
FUZZ_SRC := test_format_fuzz.cpp
FUZZ_HEADERS := $(wildcard ../include/edgevector/*.hpp)

RELEASE_TARGETS := test_quantize_math_release$(EXE) \
test_mmap_storage_release$(EXE) \
test_hnsw_graph_release$(EXE) \
test_integration_release$(EXE) \
test_itq_rotation_release$(EXE)
test_itq_rotation_release$(EXE) \
test_format_fuzz_release$(EXE)

.PHONY: all run release run-release bench clean

all: $(TARGET) $(STORAGE_TARGET) $(HNSW_TARGET) $(INTEG_TARGET) $(ITQ_TARGET)
all: $(TARGET) $(STORAGE_TARGET) $(HNSW_TARGET) $(INTEG_TARGET) $(ITQ_TARGET) $(FUZZ_TARGET)

$(TARGET): $(SRC) $(HEADERS)
$(CXX) $(CXXFLAGS) $(INCLUDES) $(SRC) -o $(TARGET)
Expand All @@ -78,13 +83,17 @@ $(INTEG_TARGET): $(INTEG_SRC) $(INTEG_HEADERS)
$(ITQ_TARGET): $(ITQ_SRC) $(ITQ_HEADERS)
$(CXX) $(CXXFLAGS) $(INCLUDES) $(ITQ_SRC) -o $(ITQ_TARGET)

$(FUZZ_TARGET): $(FUZZ_SRC) $(FUZZ_HEADERS)
$(CXX) $(CXXFLAGS) $(INCLUDES) $(FUZZ_SRC) -o $(FUZZ_TARGET)

# make stops at the first recipe line that exits non-zero, so this fails fast.
run: $(TARGET) $(STORAGE_TARGET) $(HNSW_TARGET) $(INTEG_TARGET) $(ITQ_TARGET)
run: $(TARGET) $(STORAGE_TARGET) $(HNSW_TARGET) $(INTEG_TARGET) $(ITQ_TARGET) $(FUZZ_TARGET)
$(RUNNER) ./$(TARGET)
$(RUNNER) ./$(STORAGE_TARGET)
$(RUNNER) ./$(HNSW_TARGET)
$(RUNNER) ./$(INTEG_TARGET)
$(RUNNER) ./$(ITQ_TARGET)
$(RUNNER) ./$(FUZZ_TARGET)

# --- release (-DNDEBUG) -----------------------------------------------------

Expand All @@ -105,12 +114,16 @@ test_integration_release$(EXE): $(INTEG_SRC) $(INTEG_HEADERS)
test_itq_rotation_release$(EXE): $(ITQ_SRC) $(ITQ_HEADERS)
$(CXX) $(RELEASE_FLAGS) $(INCLUDES) $(ITQ_SRC) -o $@

test_format_fuzz_release$(EXE): $(FUZZ_SRC) $(FUZZ_HEADERS)
$(CXX) $(RELEASE_FLAGS) $(INCLUDES) $(FUZZ_SRC) -o $@

run-release: $(RELEASE_TARGETS)
$(RUNNER) ./test_quantize_math_release$(EXE)
$(RUNNER) ./test_mmap_storage_release$(EXE)
$(RUNNER) ./test_hnsw_graph_release$(EXE)
$(RUNNER) ./test_integration_release$(EXE)
$(RUNNER) ./test_itq_rotation_release$(EXE)
$(RUNNER) ./test_format_fuzz_release$(EXE)

# --- benchmark (always -DNDEBUG: assert-enabled numbers are meaningless) ----

Expand All @@ -124,4 +137,4 @@ bench: $(BENCH_TARGET)

clean:
rm -f $(TARGET) $(STORAGE_TARGET) $(HNSW_TARGET) $(INTEG_TARGET) \
$(ITQ_TARGET) $(RELEASE_TARGETS) $(BENCH_TARGET)
$(ITQ_TARGET) $(FUZZ_TARGET) $(RELEASE_TARGETS) $(BENCH_TARGET)
Loading
Loading