The original lance-autoresearch was one Cargo crate optimizing one Lance
kernel (PQ L2 distance). With 9+ candidate targets enumerated in the research
note, a single-crate shape doesn't scale: per-target deps will collide, the
agent's edits to one target's kernels.rs would conflict with another's lib
path, and build/test isolation is lost. Restructure into a Cargo workspace.
Layout:
research/lance-autoresearch/
├── Cargo.toml (workspace root)
├── README.md (target table, contract overview, repo layout)
├── HARNESS.md (universal loop contract every target inherits)
├── crates/
│ ├── harness-common/ (shared: SplitMix64, geomean, peak RSS,
│ │ MAX_ABS_ERR, TOPK_DIST_TOL, TIME_BUDGET_SECS)
│ └── pq-l2/ (the landed target; was the previous single crate)
└── docs/
├── design.md (rationale for workspace shape, no Target trait)
├── adding-a-target.md (step-by-step workflow for new targets)
└── targets/pq-l2.md (per-target capsule)
Decisions documented in docs/design.md:
- Workspace, not single crate: per-target Cargo.toml so deps don't collide;
per-target src tree so agent edits don't conflict; per-target build/test
isolation for faster agent iteration.
- harness-common as a plumbing-only crate (PRNG, geomean, peak RSS, tolerance
constants, time budget). Intentionally NO Target trait - decode kernel
signatures and distance kernel signatures differ enough that a unifying
trait would either bloat or require erased boxing. Each target is its own
natural shape.
- Per-target program.md + shared HARNESS.md: the loop contract is universal,
the priors and API spec are per-target. Two files instead of one because
copy-pasting the universal loop into every program.md would drift.
pq-l2 refactor:
- src/* moved into crates/pq-l2/src/* via git mv (preserves history)
- crate renamed lance-autoresearch -> pq-l2
- SplitMix64, geomean, peak_rss_mb, MAX_ABS_ERR, TOPK_DIST_TOL,
TIME_BUDGET_SECS now imported from harness-common (drops ~70 lines of
duplication that would have been copy-pasted into every new target)
- program.md trimmed: setup/loop/hygiene moved to HARNESS.md; only the
PQ-L2-specific API contract and SIMD priors remain
- Cargo.toml depends on harness-common via path; workspace.dependencies
pins criterion uniformly across targets
The 9 candidate targets from the research note (A1 cosine/dot/hamming, A2
IVF partition select, A3 FTS BM25, A4 bitpack decode, A5 dictionary decode,
A6 FSST decode, A7 take/gather, A8 predicate eval, A9 posting list intersect,
A10 top-K merge) are listed in README.md's target table as "candidate"; each
gets a docs/targets/<name>.md capsule when it's spun up. docs/adding-a-target.md
documents the cp -r + edit-Cargo.toml + rewrite-three-files workflow.
Verified end-to-end:
- cargo build --release: clean, both crates compile
- cargo clippy --release --workspace --all-targets -- -D warnings: clean
- cargo test --release --workspace: 6/6 pass (4 harness-common + 2 pq-l2)
- cargo run --release --bin run_experiment -p pq-l2: correctness pass,
geomean ~880k ns, exit 0, ~30s wall-clock
- omnigraph parent workspace unchanged (research/ excluded as before)
https://claude.ai/code/session_01Aq8kBUcjmEPobcEufnWbW5
6 KiB
HARNESS — shared loop contract for every lance-autoresearch target
This document is the universal part of every target's agent instructions. Each
target's program.md is a thin layer of target-specific priors and API spec
on top of the conventions below. The agent reads HARNESS.md and the target's
program.md at the start of every session.
What this harness is
A single agent (you) edits one file in one target crate to optimize a Lance kernel. Per trial, you build, run a binary that exercises the kernel against diverse inputs, parse a fixed-format output block, and decide keep-or-revert.
This is a Karpathy-style autoresearch loop. It assumes:
- Per-trial eval is seconds-scale. Long enough to measure, short enough to iterate hundreds of times in a session.
- The kernel has a deterministic correctness oracle — a scalar reference that produces the same answer to compare against.
- The optimization target is dataset-independent: the harness generates diverse inputs each trial, so wins generalize across distributions and shapes by construction.
Targets that don't fit these constraints (index-build parameter tuning,
plan-patching, anything where eval is minutes-to-hours) belong in the
BauplanLabs tournament-loop shape, not this harness. See docs/design.md for
the boundary.
What's editable, per target
| Path | Mutability | Why |
|---|---|---|
crates/<target>/src/kernels.rs |
mutable | Your playground. The whole point. |
crates/<target>/src/reference.rs |
immutable | The oracle. Touching it makes wins meaningless. |
crates/<target>/src/inputs.rs |
immutable | The fixture generator. Touching it makes timings incomparable across trials. |
crates/<target>/src/lib.rs |
immutable | Shared types pinned by the bench (PqShape etc.). |
crates/<target>/src/bin/run_experiment.rs |
immutable | The trial harness. |
crates/<target>/benches/*.rs |
immutable | Criterion bench, optional read-only reference. |
crates/<target>/Cargo.toml |
immutable | Adding deps changes the optimization target. |
crates/<target>/program.md |
human-iterated between runs | Not edited by you in-loop; the human refines it. |
crates/<target>/results.tsv |
append-only | Your audit log. Gitignored. |
crates/harness-common/** |
immutable | Workspace-shared infrastructure. |
HARNESS.md (this file) |
immutable | Workspace-shared loop contract. |
You may add #[cfg(test)] mod tests { ... } inside kernels.rs for in-file
property checks. You may NOT add new crate dependencies. You may NOT use
unsafe-only-on-broken-assumptions tricks (e.g., assuming a fixture invariant
that holds today but isn't documented).
The metric
Every target's run_experiment binary prints a fixed-format output block ending
with these universal fields:
correctness:—passorfail. Set by comparing your kernel against the scalar reference on every input the bench generates.geomean_ns_per_*:— geometric mean of per-operation wall-clock across all timed operations.worst_ns_per_*:— slowest combo's geomean.peak_mem_mb:— process RSS high-water-mark.total_seconds:— trial wall-clock.
A kernel is kept iff:
correctness: pass(any failure →std::process::exit(2)).geomean_ns_per_*strictly better than the previous best-kept kernel (allow ~1% noise band).worst_ns_per_*≤ 1.05 × the previous best-kept kernel's worst.total_seconds≤ 600 (the per-trial cap; exceed it →std::process::exit(3)).- Build clean:
cargo build --releaseandcargo clippy --release --all-targets -- -D warningsboth succeed.
Ties break toward simpler code: same speed within ~3% noise → fewer lines /
less unsafe wins.
The loop
After reading HARNESS.md and the target's program.md:
-
Setup (once per session). Confirm
results.tsvexists; if not, create it with a per-target header (the target'sprogram.mddefines the columns). Run the baseline trial:cargo run --release --bin run_experiment -p <target> > run.log 2>&1Append a row tagged
keep=baselineand commit it. -
Observe state. Read the last ~5 rows of
results.tsv. Note which ideas have been tried, what won, what regressed. Form one hypothesis with one sentence stating the change and the predicted effect on speed and correctness. -
Edit
kernels.rs. Keep the diff focused on the one hypothesis. -
Build and lint.
cargo build --release cargo clippy --release --all-targets -- -D warningsIf either fails, fix and retry. Do not commit broken state.
-
Run the trial.
cargo run --release --bin run_experiment -p <target> > run.log 2>&1 -
Parse and decide. Extract the universal fields plus any per-target fields. Compute deltas vs. the last-kept row. Apply the keep criteria above.
-
Log. Append one row to
results.tsvmatching the target's header. -
Commit. One-line message describing the change and the headline number, e.g.
transpose codebook in new(); 18.2k → 14.1k geomean ns (worst -8%). -
Hygiene.
- Always commit
kernels.rschanges; never commitresults.tsvorrun.log(gitignored). - If a change fails to build, do not commit. Iterate or revert cleanly.
- If two consecutive ideas regress, take a beat: re-read the last ~10 rows and update your mental model before proposing the next.
- Per-trial cap: 10 minutes. If
cargo runis still going after 10 min, kill it and mark the trial astimeout.
- Always commit
Never stop
Keep going until interrupted. Each loop iteration is one hypothesis, one edit, one measurement, one commit. No multi-step plans across iterations.
Working across multiple targets
If a session spans multiple targets, work on one target per session. Don't
edit kernels.rs in two crates between commits — the agent's mental model is
shared but the keep-decision is per-target. Pick a target, do a session there,
commit, switch.
The human is responsible for selecting which target to work on next. Don't proactively switch targets unless the user asks.