Event-sourced campaign ledger for AI-driven scientific discovery — 11 trajectory analytics from the Bayesian optimization and self-driving lab literature.
Part of the Deep Science Tooling series: production-grade CLI tools for scientific research.
AI-driven discovery loops (materials, chemistry, drug candidates) generate and screen candidates per iteration. The loop is fragile in ways current tooling can't surface: surrogate-model exploitation, diversity collapse, repeated failed experiments, and poor budget visibility.
Existing tools record independent runs. A discovery campaign is a causal loop (propose → test → learn → decide). The questions that matter — is the surrogate decoupling from reality? is the search collapsing? — are functions of the trajectory, not properties of current state. nilvar models the campaign as a causal process and mines it for discovery-health signals.
Researchers already track experiments with other tools. nilvar occupies a different niche: it models the campaign as a causal trajectory, not a collection of independent runs.
| Tool | What it tracks | Where nilvar differs |
|---|---|---|
| MLflow / W&B | Model metrics, hyperparameters, individual experiment runs | nilvar tracks the full propose-test-learn-decide loop across iterations; its analytics detect trajectory-level pathologies (proxy-gaming, diversity collapse) that per-run trackers cannot surface |
| Electronic lab notebooks | Lab procedures, observations, results in human-readable form | nilvar is machine-readable, event-sourced, and hash-chained for tamper detection; events are structured data, not prose |
| Spreadsheets / AirTable | Ad-hoc campaign tracking, manual entry | No immutability guarantees, no audit trail, no built-in analytics; a shared spreadsheet cannot prove it was not edited after the fact |
| Custom scripts | Whatever you build | nilvar ships 11 trajectory analytics grounded in the Bayesian optimization literature, plus counterfactual replay, out of the box |
| Ax / BoTorch / Dragonfly | Surrogate models and acquisition functions (the optimization engine) | nilvar does not replace the engine; it sits alongside it, recording what the engine proposed and what reality returned, then auditing the gap |
nilvar is complementary to most of these. It ingests predictions from your surrogate model of choice and records outcomes from your lab or simulation, then mines the trajectory for signals that no individual tool above is designed to catch.
- A. Getting Started
- B. Architecture
- C. Analytics
- D. Compliance Alignment
- E. Reference
- F. Literature
- G. Performance
- H. Development & Contributing
pip install -e ".[all]"This installs the CLI, API server, analytics (FAISS), trajectory charts, and YAML ingest support.
Individual extras
| Extra | What it adds |
|---|---|
cli |
Typer CLI (nilvar command) |
api |
FastAPI server + uvicorn |
analytics |
FAISS for vector similarity search |
viz |
Matplotlib trajectory charts |
ingest |
YAML file ingest support |
dev |
Tests, linting, type checking |
# Core only (event store + SDK + numpy-backed analytics)
pip install -e .
# Development
pip install -e ".[dev]"# Initialize a campaign
nilvar init --campaign-id perovskite-opt --goal "maximize bandgap" --budget-cap 50000.0
# Ingest candidate data from a file
nilvar ingest --file candidates.json
# Record a validated lab result
nilvar record-result --candidate-id CsPbI3 --value 1.68 --uncertainty 0.05 --iteration 1
# Generate a campaign report
nilvar report
nilvar report --format html --output report.html
# Run analytics
nilvar counterfactual --policy greedy --top-k 1SDK Quick Start (notebooks/scripts)
from nilvar.sdk import CampaignClient
client = CampaignClient("my_campaign.db")
client.create_campaign("perovskite-opt", "maximize bandgap", 50000.0)
# Your surrogate proposes candidates
client.propose("CsPbI3", {"Cs": 1, "Pb": 1, "I": 3}, 1.73, "v1.0", iteration=1)
# Lab results come back
client.start_experiment("CsPbI3", cost=5000.0, iteration=1)
client.record_result("CsPbI3", value=1.68, uncertainty=0.05, iteration=1)
# Check for proxy gaming (Goodhart's Law)
verdict = client.proxy_gaming()
print(verdict.verdict) # "OK" or "GAMING"
# Check for diversity collapse
verdict = client.diversity()
print(verdict.verdict) # "OK" or "COLLAPSING"
# Full campaign report
print(client.report())See examples/basic_campaign.py for a complete 3-iteration campaign, or examples/notebook_demo.ipynb for an interactive Jupyter walkthrough.
You can also run the notebook directly in Google Colab — no local install required.
For a realistic 5-iteration perovskite campaign that exercises every CLI command (CSV ingestion, failures, counterfactuals, reports), see the smoke test fixtures. The accompanying analytics guide explains how proxy gaming happens, what corrections exist, and how counterfactual replay quantifies the cost.
Every campaign fact is recorded as an immutable, hash-chained event. The write side appends events; the read side folds them into disposable projections that can be rebuilt from scratch at any time. New analytics can be added retroactively over the full history — no migrations needed.
WRITE SIDE READ SIDE
┌──────────────────────────────────────┐ ┌─────────────────────────────────────────────────┐
│ │ │ │
│ ┌─────────────────────────────────┐ │ │ ┌─────────────────────────────────────────────┐│
│ │ Commands (CLI / SDK / API) │ │ │ │ Projectors (rebuild from seq 0) ││
│ │ │ │ │ │ ││
│ │ • propose • record-result │ │ │ │ • timeline (per-candidate event history) ││
│ │ • start-exp • record-failure │ │ │ │ • negledger (failure ledger by class) ││
│ │ • select • reject │ │ │ │ • budget (cumulative spend vs cap) ││
│ │ • close-iter • update-model │ │ │ │ • iterations (per-iteration summary) ││
│ └───────────────┬─────────────────┘ │ │ └──────────────────────┬──────────────────────┘│
│ │ │ │ │ │
│ ┌───────────────▼─────────────────┐ │ │ ┌──────────────────────▼──────────────────────┐│
│ │ Handler (Pydantic v2) │ │ │ │ Analytics (numpy / FAISS) ││
│ │ │ │ │ │ ││
│ │ • validate schema │ │ │ │ • proxy-gaming • calibration drift ││
│ │ • content-hash dedup │ │ │ │ • diversity • Pareto front ││
│ │ • causal link (caused_by) │ │ │ │ • discovery rate • reproducibility ││
│ │ • hash-chain append │ │ │ │ • model learning • acquisition efficiency ││
│ └───────────────┬─────────────────┘ │ │ │ • regret • explore/exploit ││
│ │ │ │ │ • data quality • counterfactual replay ││
└──────────────────┼───────────────────┘ │ └─────────────────────────────────────────────┘│
│ └─────────────────────────────────────────────────┘
▼ ▲
┌────────────────────────────┐ │
│ EVENT STORE (SQLite) │ fold from seq 0 │
│ │──────────────────────┘
│ • append-only, immutable │
│ • SHA-256 hash-chained │
│ • content-hash dedup │
│ • WAL mode for concurrency │
└────────────────────────────┘
- Event sourcing: every event is immutable, causally-linked, and hash-chained (SHA-256)
- CQRS: write side (append facts) and read side (fold trajectories) are separate
- Replay: any analytic can be added later and rebuilt over full history — no migrations
- Snapshotting: projections checkpoint at seq N and replay only from N+1 forward
- Three surfaces, one handler: CLI, SDK, and API all wrap the same
CommandHandler— agent, script, and human hit the same validation and dedup logic
| Layer | Technology |
|---|---|
| Language | |
| Schemas | |
| Event store | |
| Hash chain | SHA-256, linear tamper-evident chain |
| CLI | |
| API | |
| Analytics | |
| Charts | |
| Type checking | |
| Linting |
10 event types covering the full propose → test → learn → decide cycle:
| Event | Stage | Role |
|---|---|---|
CampaignCreated |
Setup | Initialize campaign with goal and budget |
CandidateProposed |
Propose | Surrogate prediction captured at proposal time |
ExperimentStarted |
Test | Budget committed before outcome is known |
ResultRecorded |
Test | Validated measurement with uncertainty |
FailureRecorded |
Test | Classified failure (6 classes: synthesizability, stability, out_of_spec, non_novel, measurement_error, process_error) |
BudgetConsumed |
Test | Cumulative cost tracking |
ModelUpdated |
Learn | Surrogate retrained — resets proxy-gaming window |
CandidateSelected |
Decide | Decision with rationale and causal links |
CandidateRejected |
Decide | Decision with rationale and causal links |
IterationClosed |
Decide | Marks iteration boundary |
Every event is hash-chained (SHA-256) for tamper-evidence and content-hash deduped for idempotent ingest.
src/nilvar/
├── events/ # Event envelope, catalog, failure taxonomy (WRITE SIDE)
├── store/ # SQLite append-only event store (WRITE SIDE)
├── commands/ # Command handler with validation + dedup (WRITE SIDE)
├── projections/ # Projector framework + core projections (READ SIDE)
├── analytics/ # 11 trajectory analytics (READ SIDE)
├── decisions/ # Counterfactual replay, pluggable policies
├── api/ # FastAPI (one API surface)
├── sdk/ # Python SDK for notebooks/scripts
├── cli/ # Typer CLI
└── report/ # Markdown + HTML report generators
nilvar's differentiators — trajectory analytics that CRUD experiment trackers structurally cannot provide.
BO = Bayesian optimisation — the closed-loop surrogate-driven search that nilvar records.
| Analytic | What it detects | Signal | Field relevance |
|---|---|---|---|
| Proxy gaming | Surrogate predictions rising while validated results stay flat | Goodhart's Law in action | All BO |
| Diversity collapse | Candidate proposals contracting to a narrow region | Exploration dying | All BO |
| Discovery rate | Cost per validated hit flatlines; zero-result iterations | Campaign exhaustion | All BO |
| Calibration drift | Model uncertainty estimates diverge from observed errors | Broken explore/exploit | Materials, drug discovery |
| Pareto front | Multi-objective hypervolume stops expanding | Tradeoff surface stalled | Materials, drug discovery |
| Reproducibility | Replicate measurements show high variance (CV) | Noisy labels | Materials, drug discovery |
| Model learning | MAE fails to improve across surrogate retrains | Surrogate plateaued | All BO |
| Acquisition efficiency | Predicted rankings don't match actual outcomes (Spearman rho) | Wrong candidate ordering | All BO |
| Cumulative regret | Gap between best-known and selected values grows linearly | Non-converging acquisition | All BO |
| Explore/exploit ratio | Proposals consistently cluster at one extreme of the exploitation spectrum | Imbalanced search | All BO |
| Data quality | Anomaly rate per iteration exceeds threshold (z-score outliers) | Instrument/reagent drift | Materials, drug discovery |
| Counterfactual replay | Would a different policy have found the winner sooner? | Decision quality | All BO |
Install the viz extra for matplotlib-based trajectory visualisation:
pip install -e ".[viz]"Charts are embedded automatically in HTML reports (nilvar report --format html). In notebooks, use the SDK plotting helpers (try them in Colab):
fig = client.plot_proxy_gaming() # predicted vs validated divergence
fig = client.plot_diversity() # candidate spread per iteration
fig = client.plot_budget() # cumulative burn vs cap
fig = client.plot_discovery_rate() # results/failures + cost efficiency
fig = client.plot_calibration() # coverage rate over iterations
fig = client.plot_pareto() # hypervolume progress
fig = client.plot_reproducibility() # per-candidate CV for replicates
fig = client.plot_model_learning() # MAE across model versions
fig = client.plot_acquisition_efficiency() # Spearman rank correlation
fig = client.plot_regret() # per-iteration + cumulative regret
fig = client.plot_explore_exploit() # exploitation ratio trend
fig = client.plot_data_quality() # anomaly rate per iterationDiversity spread is computed as mean pairwise Euclidean distance across candidate feature vectors. numpy is a core dependency — vectorised O(n^2) broadcasting replaces the pure-Python loop. For campaigns with large candidate pools, install the analytics extra for FAISS approximate nearest-neighbor acceleration.
Replays the recorded campaign through alternate decision policies (greedy or random) and compares: when did each policy find the best candidate, and at what cost? This requires the full causal sequence of what was known at each decision point — a capability CRUD structurally cannot provide.
nilvar's event-sourcing architecture aligns with scientific data integrity standards by design. These properties are structural consequences of event sourcing, not bolted-on features.
| Standard | How nilvar aligns | What's not covered |
|---|---|---|
| ALCOA+ (data integrity) | Attributable (actor field), Contemporaneous (ts + server-side recorded_at), Original (append-only log + raw_data_ref), Accurate (hash chain + unit fields), Legible (nilvar export evidence bundle) |
Enduring (archival/WORM), Available (access controls) |
| 21 CFR Part 11 (FDA electronic records) | Immutable audit trail with tamper-evident hash chain | E-signatures, access controls, system validation (organizational controls beyond a local-first tool) |
| FAIR (scientific data) | Findable (campaign URN), Accessible (REST API), Interoperable (JSON schema), Reusable (license, provenance, replay from seq 0) | Persistent DOIs, standard ontologies, JSON-LD export |
| GAMP 5 (pharma system validation) | Category 5 classification with validation pack: IQ/OQ/PQ protocols, traceability matrix | Executed validation is the adopting organization's responsibility |
nilvar provides the technical substrate for compliance; the adopting organization layers identity, access controls, SOPs, and validation execution on top.
GAMP 5 validation pack: docs/gamp/ contains the system classification, traceability matrix, and executed IQ/OQ/PQ protocols with actual results (53 steps passed, 16 Docker-dependent steps deferred to CI). Organizations adopting nilvar in GxP environments should re-execute these protocols in their target environment.
Standards assessed and documented as not applicable: EU GMP Annex 11, ICH GCP E6, HIPAA, GDPR, ISO 27001/SOC 2, ISO 42001/AI governance, export controls. See docs/compliance_reference.md for rationale on each.
nilvar --version # print version and exit
nilvar --verbose ... # enable debug logging
nilvar --quiet ... # suppress info, show warnings/errors onlyWrite commands:
# Initialize a campaign database
nilvar init --campaign-id perovskite-opt --goal "maximize bandgap" --budget-cap 50000.0
# Ingest events from JSON/CSV/YAML
nilvar ingest --file data.json
# Record a validated result
nilvar record-result --candidate-id CsPbI3 --value 1.68 --uncertainty 0.05 --iteration 1
# Record a failure
nilvar record-failure --candidate-id CsPbBr3 --failure-class synthesizability --iteration 1 --detail "Precursor unavailable"Read commands:
# Campaign report (markdown, HTML, or JSON)
nilvar report
nilvar report --format html --output report.html
nilvar report --format json
# Verify hash chain integrity (shows event breakdown, actors, time range)
nilvar audit
nilvar audit --output json
# Export portable evidence bundle for auditors
nilvar export --output bundle.json
# Counterfactual replay
nilvar counterfactual --policy greedy --top-k 1
nilvar counterfactual --policy random --seed 42SDK Reference
from nilvar.sdk import CampaignClient
client = CampaignClient("campaign.db")Write operations:
| Method | Description |
|---|---|
client.create_campaign(id, goal, budget_cap) |
Initialize a new campaign |
client.propose(candidate_id, features, predicted_value, model_version, iteration) |
Record a surrogate prediction |
client.start_experiment(candidate_id, cost, iteration) |
Commit budget before outcome |
client.record_result(candidate_id, value, uncertainty, iteration) |
Record a validated measurement |
client.record_failure(candidate_id, failure_class, iteration, detail) |
Record a classified failure |
client.select(candidate_id, rationale, model_version, iteration) |
Select a candidate with rationale |
client.reject(candidate_id, rationale, model_version, iteration) |
Reject a candidate with rationale |
client.update_model(model_version, iteration, summary) |
Record surrogate retrain |
client.close_iteration(iteration, summary) |
Mark iteration boundary |
Read operations:
| Method | Description |
|---|---|
client.report(format="markdown") |
Campaign report ("markdown" or "html") |
client.iterations() |
Per-iteration summary with candidates, results, costs |
Analytics:
| Method | Returns | Signal |
|---|---|---|
client.proxy_gaming(window=3) |
ProxyGamingVerdict |
Surrogate predictions rising while validated results flat |
client.diversity(window=3) |
DiversityVerdict |
Candidate proposals contracting to narrow region |
client.discovery_rate() |
DiscoveryRateVerdict |
Cost per hit flatlines; zero-result iterations |
client.calibration(window=3) |
CalibrationVerdict |
Uncertainty estimates diverge from observed errors |
client.pareto(objectives=None) |
ParetoVerdict |
Multi-objective hypervolume stops expanding |
client.reproducibility(cv_threshold=0.3) |
ReproducibilityVerdict |
Replicate measurements show high variance |
client.model_learning(window=3) |
ModelLearningVerdict |
MAE fails to improve across retrains |
client.acquisition_efficiency(window=3) |
AcquisitionEfficiencyVerdict |
Predicted rankings don't match actual outcomes |
client.regret(window=3) |
RegretVerdict |
Gap between best-known and selected grows |
client.explore_exploit(window=3) |
ExploreExploitVerdict |
Search balance too extreme |
client.data_quality(window=3) |
DataQualityVerdict |
Anomaly rate exceeds threshold |
Counterfactual replay:
result = client.counterfactual_replay(policy="greedy", top_k=1)
print(f"Original found best at iteration {result['original_found_at_iteration']}")
print(f"Greedy would find it at iteration {result['alternate_found_at_iteration']}")
print(f"Iterations saved: {result['iterations_saved']}")| Parameter | Description |
|---|---|
policy |
"greedy" (exploitation baseline) or "random" (exploration baseline) |
top_k |
How many candidates the alternate selects per iteration |
seed |
RNG seed for random policy (ignored for greedy) |
Replays the campaign through an alternate decision policy: did the alternate find the best candidate sooner or cheaper? Only compares paths through candidates that were actually tested.
API Reference
The API runs via Docker Compose (see Docker below). For local development:
pip install -e ".[api]"
uvicorn nilvar.api.app:app --reloadInteractive docs at http://localhost:8000/docs (Swagger UI).
Write endpoints:
| Method | Endpoint | Description |
|---|---|---|
POST |
/campaigns |
Create campaign |
POST |
/candidates/propose |
Propose a candidate |
POST |
/experiments/start |
Start experiment (commit budget) |
POST |
/results |
Record validated result |
POST |
/failures |
Record classified failure |
POST |
/candidates/select |
Select candidate with rationale |
POST |
/candidates/reject |
Reject candidate with rationale |
POST |
/model/update |
Record surrogate retrain |
POST |
/iterations/close |
Close iteration |
Read endpoints:
| Method | Endpoint | Description |
|---|---|---|
GET |
/report?format=markdown |
Campaign report (markdown or html) |
GET |
/timeline/{candidate_id} |
Per-candidate event timeline |
GET |
/negledger |
Failure ledger |
GET |
/budget |
Budget state |
GET |
/iterations |
Per-iteration summary |
Analytics endpoints:
| Method | Endpoint | Description |
|---|---|---|
GET |
/analytics/proxy-gaming?window=3 |
Proxy-gaming detector |
GET |
/analytics/diversity?window=3 |
Diversity-collapse monitor |
GET |
/analytics/discovery-rate |
Discovery rate analysis |
GET |
/analytics/calibration?window=3 |
Calibration drift detector |
GET |
/analytics/pareto?window=3 |
Pareto front progress |
GET |
/analytics/reproducibility?cv_threshold=0.3 |
Reproducibility monitor |
GET |
/analytics/model-learning?window=3 |
Model learning rate |
GET |
/analytics/acquisition-efficiency?window=3 |
Acquisition efficiency |
GET |
/analytics/regret?window=3 |
Cumulative regret |
GET |
/analytics/explore-exploit?window=3 |
Explore/exploit ratio |
GET |
/analytics/data-quality?window=3 |
Data quality / anomaly rate |
GET |
/analytics/counterfactual?policy=greedy&top_k=1 |
Counterfactual replay |
Operations:
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Health check (returns {"status":"ok","events":"<count>"}) |
GET |
/audit |
Verify hash chain integrity |
Docker
The API server runs via Docker Compose. No Python install required.
# 1. Copy the example config (optional — defaults work out of the box)
cp .env.example .env
# 2. Start the API
docker compose up -d
# 3. Verify it's running
curl http://localhost:8000/health
# {"status":"ok","events":"0"}Configuration:
| Variable | Default | Description |
|---|---|---|
NILVAR_DB |
/data/nilvar.db |
SQLite database path inside the container |
NILVAR_LOG_LEVEL |
INFO |
Python logging level |
NILVAR_CORS_ORIGINS |
[] |
JSON array of allowed CORS origins |
Set variables in .env (loaded automatically by Compose) or inline: NILVAR_LOG_LEVEL=DEBUG docker compose up. See .env.example for the full reference.
Data persistence: The SQLite database is stored in a Docker named volume (nilvar-data), mounted at /data inside the container. Data survives container restarts and image rebuilds.
# Back up the database
docker compose cp nilvar-api:/data/nilvar.db ./backup.dbProduction: For deployments behind a reverse proxy, configure rate limiting and TLS at the proxy layer (nginx, Caddy, Cloudflare). The nilvar API is stateless HTTP.
nilvar's trajectory analytics are grounded in the Bayesian optimization, self-driving lab, and autonomous experimentation literature.
| Paper | What it grounds |
|---|---|
| Srinivas et al. 2010, "Gaussian Process Optimization in the Bandit Setting: No Regret and Experimental Design" (ICML) | Regret bounds, exploration-exploitation theory — foundational GP-UCB paper |
| Shahriari et al. 2016, "Taking the Human Out of the Loop: A Review of Bayesian Optimization" (IEEE) | Comprehensive BO survey; explore-exploit tradeoff framework |
| Garnett 2023, Bayesian Optimization (Cambridge University Press) | Textbook treatment of acquisition function evaluation and BO campaign design |
| Frazier 2018, "A Tutorial on Bayesian Optimization" | Sample efficiency and regret definitions in BO context |
| Rasmussen & Williams 2006, Gaussian Processes for Machine Learning | Surrogate model comparison and uncertainty quantification |
| Paper | Signal |
|---|---|
| Amodei et al. 2016, "Concrete Problems in AI Safety" (§3) | Proxy gaming / Goodhart's Law — surrogate optimizes its own score, not reality |
| Kuleshov et al. 2018, "Accurate Uncertainties for Deep Learning Using Calibrated Regression" (ICML) | Calibration drift — surrogate confidence intervals vs observed hit rates |
| Guo et al. 2017, "On Calibration of Modern Neural Networks" (ICML) | Expected Calibration Error (ECE) definition |
| Foldager et al. 2023, "On the Role of Model Uncertainties in Bayesian Optimisation" (UAI) | How miscalibration directly degrades BO performance |
| Zitzler & Thiele 1999, "Multiobjective Evolutionary Algorithms: A Comparative Case Study" (IEEE Trans. Evol. Computation) | Hypervolume indicator for multi-objective Pareto front progress |
| Daulton et al. 2021, "Parallel Bayesian Optimization of Multiple Noisy Objectives with Expected Hypervolume Improvement" (NeurIPS) | qNEHVI — multi-objective BO in BoTorch |
| Makarova et al. 2024, "Stopping Bayesian Optimization with Probabilistic Regret Bounds" | Regret-based convergence and stopping criteria |
| Paper | Relevance |
|---|---|
| Abolhasani & Kumacheva 2023, "The rise of self-driving labs in chemical and materials sciences" (Nature Synthesis) | Typed failure classification in closed-loop discovery |
| Granda et al. 2018, "Controlling an organic synthesis robot with machine learning" (Nature) | Early SDL tracking measurement variance and reproducibility |
| Burger et al. 2022, "Autonomous Chemical Experiments: Challenges and Perspectives" (Accounts of Chemical Research) | SDLs tracking replicate variance as system health |
| Kalinin et al. 2024, "Dual-GP active oversight framework" (npj Computational Materials) | Real-time surrogate quality monitoring in autonomous experiments |
| Bartel et al. 2020, "A critical examination of compound stability predictions from ML formation energies" (npj Computational Materials) | Stability vs performance tradeoffs in materials discovery |
| Paper | Relevance |
|---|---|
| Bellamy et al. 2021, "Bias-free multi-objective active learning" (Nature Communications) | Multi-objective BO applied to materials discovery SDLs |
| Warmuth et al. 2003, "Active Learning with SVMs in the Drug Discovery Process" (JCIM) | Learning curves applied to drug discovery campaigns |
| Baker 2016, "1,500 scientists lift the lid on reproducibility" (Nature) | The reproducibility crisis — why measurement quality matters |
| Settles 2012, Active Learning (monograph) | Learning curves and model learning rate formalization |
Benchmarked on a 1,000-event in-memory SQLite store (100 iterations, 10 candidates each). Numbers are median of 5 runs on a single core.
| Operation | Throughput | Latency |
|---|---|---|
Cold import (from nilvar) |
~310 ms | |
| Event writes (hash-chained appends) | ~175 events/sec | ~6 ms/event |
| Full replay (4 projectors from seq 0) | ~55,000 events/sec | ~18 ms / 1k events |
| Report generation (markdown) | < 1 ms | |
| Hash chain verification | ~43,000 events/sec | ~23 ms / 1k events |
Replay and verification are I/O-bound on SQLite reads; in-memory stores hit higher throughput. Write throughput is dominated by per-event SHA-256 hashing and SQLite fsync.
make dev # install with all dependencies
make check # run full quality gate (lint + format + typecheck + test)All available targets:
| Command | What it does |
|---|---|
make help |
Show all available targets |
make install |
Install core package |
make dev |
Install with all dev dependencies |
make check |
Run full quality gate (lint + format + typecheck + test) |
make lint |
Run ruff linter |
make format |
Check code formatting |
make typecheck |
Run mypy strict type checking |
make test |
Run tests (374 tests) |
make coverage |
Run tests with coverage report (93%, 80% gate) |
make audit |
Audit dependencies for vulnerabilities |
make clean |
Remove build artifacts |
bash fixtures/smoke_test/run_smoke_test.sh |
Run end-to-end smoke test (details) |
See CONTRIBUTING.md for the full development workflow, architecture constraints, and commit format.
MIT