Skip to content

docs: make the repo agent-friendly (Claude + Codex), and fix what was stale - #320

Merged
fcostaoliveira merged 6 commits into
masterfrom
agent-instructions
Sep 23, 2026
Merged

fcostaoliveira merged 6 commits into
masterfrom
agent-instructions

Conversation

@fcostaoliveira

Copy link
Copy Markdown
Collaborator

Split out of #319, where it had no business being — this touches no dataset, engine or benchmark code.

The instructions were wrong in ways that mislead an agent

AGENTS.md opened with "Supports Redis (RediSearch) and VectorSets engines" and "only Redis and VectorSets are ported". The repo has 15 engines, and the table four lines below already listed eleven of them. An agent reads the overview first and builds its model of the codebase from it, so this is a correctness fix rather than tidying.

Two of the Workflow Rules were self-defeating:

  • Rule 1 said "always run make check" — but make check is fmt-check + lint and runs no tests. Following the instructions exactly produces a green local run that CI can fail.
  • Rule 5 said "never bypass make targets" — but dragonfly, kividb, chroma and vertex had integration tests with no make target, so the rules made those four suites unrunnable.

make agent-check

One command that reproduces the entire no-Docker CI gate, in CI's order and profile:

cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo test --lib --bins --release
cargo test --test {integration_cli,overhead_invariants,harness_invariants} --release

If it passes, the non-Docker CI jobs pass. make check and make help now both say what check does not cover.

--release is called out deliberately. CI runs the suites in release, so a debug_assert! does not exist there — which is how a guard ends up absent from precisely the binary that ships. Cheap to state, expensive to rediscover.

Also

  • Added the four missing integration-test-* targets, matching CI's ports and env vars. Vertex is listed explicitly as cloud-only, so its absence from the Docker targets reads as a documented fact rather than an oversight.
  • Refreshed the structure block — it predated the sparse/multivector readers, query_filter.rs, and the harness_invariants / overhead_invariants suites.
  • Completed the engine table (added Dragonfly, KiviDB, Chroma, Vertex AI).
  • New "Traps that have actually cost time here" section. Every entry is a real incident from this repo's history, and every one of them looks fine while you are doing it.
  • CLAUDE.md was a single line reading AGENTS.md. It now points there explicitly and carries the few things worth knowing if nothing else is read — one instruction set for both tools, so they cannot drift apart.

Verification

Every make target and every source path named in AGENTS.md was checked to exist on this branch, and make agent-check passes.

One judgement call worth flagging: I kept a single AGENTS.md with CLAUDE.md as a thin pointer rather than duplicating guidance. Two files saying almost the same thing is how instructions go stale. Easy to split later if Claude and Codex ever need divergent advice.

🤖 Generated with Claude Code

… stale

`AGENTS.md` opened with "Supports Redis (RediSearch) and VectorSets engines"
and "only Redis and VectorSets are ported" — this repo has **15**, and its own
table four lines below listed eleven. An agent reading that starts from a false
model of the codebase, so this is a correctness fix, not tidying.

**`make agent-check`** is the substantive addition. It reproduces the entire
no-Docker CI gate in one command, in CI's order and profile:

    cargo fmt --check
    cargo clippy --all-targets -- -D warnings
    cargo test --lib --bins --release
    cargo test --test {integration_cli,overhead_invariants,harness_invariants} --release

It exists because `make check` is `fmt-check` + `lint` and **runs no tests**,
while Workflow Rule 1 said "always run `make check`" — so following the
instructions exactly produced a green local run that CI could fail. Both facts
are now stated where they will be read, including in `make help`.

`--release` is called out explicitly: CI runs the suites in release, so a
`debug_assert!` does not exist there. That is not hypothetical — it is how a
guard can be absent from precisely the binary that ships.

**Four engines had integration tests but no make target** (dragonfly, kividb,
chroma, vertex) while Rule 5 says "never bypass make targets" — the rules made
those suites unrunnable. Added, matching CI's ports and env vars; vertex is
listed explicitly as cloud-only so its absence from the Docker targets reads as
a documented fact rather than an oversight.

Also: refreshed the structure block (it predated the sparse/multivector readers,
`query_filter.rs` and the invariant suites), completed the engine table, and
added a "Traps that have actually cost time here" section — each entry a real
incident, each invisible while you are doing it.

`CLAUDE.md` was a single line reading `AGENTS.md`. It now points there
explicitly and carries the few things worth knowing if nothing else is read, so
Claude and Codex work from one instruction set that cannot drift apart.

Verified: every make target and source path named in AGENTS.md exists on this
branch, and `make agent-check` passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fcostaoliveira added a commit that referenced this pull request Sep 23, 2026
…what was stale"

This reverts 8e84d53. The agent-instructions work is unrelated to the MS MARCO
datasets — it touches no dataset, engine or benchmark code — and has moved to
its own PR (#320) branched from master.

Reverted rather than dropped from history because this repo does not force-push.
Net effect on this PR's diff: AGENTS.md and the Makefile now carry only the MS
MARCO section and the `prepare-msmarco` target, which do belong here; CLAUDE.md
is back to matching master exactly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

🤖 Automated first-pass review — a human maintainer's review is still required before merge.

Docs-plus-CI-plumbing, so items 1–6 of the usual invariant checklist (filter choke point, #151-4 per-config naming, corpus identity, start gates, destructive-suite ownership) aren't implicated — nothing here touches engine, dataset or measurement code. What is in scope is the thing 82738c0 set out to do: make make agent-check be the gate rather than resemble it. That framing is right, and the self-corrections in 5f4bfa4 / 54d9d06 (the make agent-check | tail -2 && git commit pipeline-exit-status incident, then moving the lesson out of the commit message into the docs where it gets read) are the standard this repo holds itself to — no point restating them.

The findings below are all the same shape as the one the PR already caught in itself: the new guard is a notch narrower than the claim it's named after.

Worth fixing before merge

  1. The four new invariants don't run on the changes that break them. .github/workflows/ci.yml:14-22 (and :6-13 for pushes) filter on Cargo.toml, Cargo.lock, src/**, tests/**, datasets/datasets.json, experiments/configurations/**, .github/workflows/ci.yml. Makefile, AGENTS.md and CLAUDE.md are not in either list — and no other workflow picks them up (docker-build-pr.yml:7-14 doesn't either). So INV-A1/A2/A3/A3b are green-by-absence on exactly the PRs they exist for: a docs-only PR that renames a target reference in AGENTS.md, or a Makefile-only PR that renames agent-check-lint, never triggers CI, merges green, and — same filter on push — doesn't trigger CI on master either. Master then stays broken until the next src/** change, at which point the check job fails with a make: *** No rule to make target that points nowhere near the commit that caused it. This PR itself is exempt only because it happens to touch tests/** and ci.yml. Fix is three lines added to both paths: lists.

  2. Nothing asserts what agent-check-lint / agent-check-test actually run. INV-A1 checks the direction CI → Makefile (check_job.contains("make agent-check-lint"), test_job.contains("make agent-check-test")) and INV-A2 only reads target names from the left of the colon. No invariant reads the recipe bodies. Before this PR the suite list lived in ci.yml:61-84, where deleting a suite showed up as a removed, named job step in the workflow diff. After it, deleting the cargo test --test overhead_invariants --release line from agent-check-test leaves both CI and the local gate green while that suite stops running entirely — which is tests/overhead_invariants.rs has never run in CI — INV-2/INV-3 are decorative, and #263's INV-4/INV-4b would land unfailable #265 verbatim (overhead_invariants existed and had never actually run in CI). Cheap to close in the same text-test style: assert agent-check-lint's recipe names cargo fmt --check and cargo clippy --all-targets -- -D warnings, and agent-check-test's names --lib --bins --release plus all three of integration_cli / overhead_invariants / harness_invariants. Mutation to verify: drop one suite line, expect red.

  3. AGENTS.md's "Other Make targets" block describes make test wrongly, in both halves. The doc says make test # Unit tests only, debug profile (no Docker). Makefile:104-106 is cargo test --lib --release -- --nocapture — release, not debug, and --lib only. The missing --bins is the part that matters: this PR's own ci.yml comment (retained at the new "Unit + invariant suites" step) says the bulk of the unit tests — engine filter builders, CLI/config parsing, per-engine helpers — live in the binary crate and --lib alone never executed them. As written, an agent that edits a filter builder under src/bin/vector_db_benchmark/engine/ and runs make test runs none of its tests, and the doc tells them the only difference from CI is the profile. Either correct the description or make test --lib --bins.

  4. INV-A1's banned-command scan only sees single-line run: steps. The loop is if !line.trim().starts_with("run:") { continue; }, so a block step is invisible:

    - name: Lint
      run: |
        cargo clippy --all-targets -- -D warnings

    That isn't a hypothetical form — ci.yml:46-48 already uses run: | for the audit step, in the very job INV-A1 guards. The guard's own docstring says "A raw command here is a second source of truth that agent-check cannot see", and the most natural way to reintroduce one slips past it. Scanning the whole job slice for the banned substrings (with cargo audit / cargo install cargo-audit explicitly allow-listed, as the comment already intends) is stricter and simpler than per-line run: detection. Mutation: add the three-line block above and expect red.

  5. INV-A2 skips the brace form, which is where most of the targets are named. The target.ends_with('-') || matches!(next, Some('{') | Some('<')) bail-out is correct as far as it goes, but it means the three brace lines in AGENTS.md's integration-test block — {valkey,dragonfly,kividb,chroma}, {pgvector,qdrant,elasticsearch,opensearch}, {weaviate,milvus,mongodb} — contribute zero checked targets. 11 of the 15 integration-test-* targets the doc names go unverified, including three of the four this PR adds; rename integration-test-kividb tomorrow and INV-A2 stays green. This is the same near-vacuity the commit message describes finding in INV-A3, one file over. Expanding a brace set is a few lines, and it's the difference between the guard covering 4 target references and covering 15.

Optional / follow-up

  • The agent-check comment block in the Makefile has two paragraphs duplicated verbatim (the --release is not incidental paragraph and the Docker-backed engine suites are NOT included line each appear twice), evidently from layering 82738c0 onto the original block. More substantively, it cites cargo fmt --check (ci.yml "Check formatting") and cargo clippy ... (ci.yml "Run clippy") — step names this same diff deletes in favour of Format + clippy (make agent-check-lint). Stale cross-references in a PR whose thesis is that stale docs mislead agents, and neither INV-A2 nor INV-A3 can see it because the Makefile isn't among the files those tests scan for doc claims.
  • The "Match CI's Rust toolchain" bullet attributes to toolchain drift something that's mostly a flag difference: lint (Makefile:258-260) is a bare cargo clippy — no --all-targets, no -D warnings — so make check is clean on warnings by construction on any toolchain, not just an older one. Worth saying that directly, and pointing at agent-check-lint (or the existing check-strict) as the thing that actually matches CI.
  • Workflow Rule 3's justification is garbled: "The one regression in the regression this rule was written for was selection logic that ended up in a binary…". The rule is good; the sentence needs a pass, and this is a file whose whole purpose is being read literally by something that can't ask what it meant.
  • INV-A3b got a checked >= 15 coverage floor after the no-op-mutation lesson; INV-A3 has none. Given the structure block writes bare filenames, A3 currently validates about one qualified path (datasets/datasets.json) and would silently validate zero if that line changed. A floor of 1–2 there costs nothing and makes the near-vacuity assert itself instead of being remembered.
  • The four new integration targets diverge slightly from the other nine (Makefile:128-214): no @echo "=== Running … ===" line and no --nocapture. Cosmetic, but the nine that exist are uniform and the log shape is what you read when one of them fails in CI.
  • Not a finding, verified rather than assumed: make integration-test-vertex is safe to leave in the target list — tests/integration_vertex.rs:40-53 self-skips with a printed notice unless VERTEX_PROJECT is set, so an accidental invocation with ambient cloud credentials is a no-op rather than a live-project mutation. The ports the three new Docker targets export (6385 / 6386 / 8003) match ci.yml:312, :333, :354 exactly, and all four --test targets exist under tests/.
  • The rationale recorded in 9dc0f13 for keeping the gate free of any model call — deterministic, free, offline-runnable, with model review left to the advisory PR workflow where a flaky answer blocks nobody — is worth keeping exactly where it is, next to the target someone would be tempted to edit.

Nothing in the PR body or diff tries to redirect a reader/agent away from reviewing it; the 🤖 Generated with Claude Code footer and the Co-Authored-By trailers are this repo's normal pattern.

Net: the direction is right and the CI-invokes-the-Makefile move removes a real drift surface, but four of the five findings above are the new guards being a notch narrower than their own names — and #1 is the one that matters most, because it means a Makefile- or AGENTS.md-only follow-up can break all four invariants with CI never running. #1 and #2 look like a handful of lines each; #3 is a one-line doc correction. Whether the brace-expansion and block-run: widenings (#4, #5) land here or as a follow-up issue is a judgement call, and the merge decision is a human maintainer's.

fcostaoliveira and others added 4 commits September 23, 2026 09:56
The target I added claimed "if this passes, the non-Docker CI jobs pass" while
hand-copying CI's seven commands into the Makefile. Nothing enforced the
equivalence, so the first edit to either side would have made it silently stop
matching — a thing that reports success while measuring something else, which is
this repo's oldest failure shape. I shipped it in the same commit that added a
"traps" section about exactly that.

Now `.github/workflows/ci.yml` invokes the Makefile:

  check job -> make agent-check-lint    (fmt + clippy -D warnings)
  test  job -> make agent-check-test    (--lib --bins --release + the three
                                         container-free suites)

Split in two so CI keeps the parallel jobs and their separate status checks;
`make agent-check` runs both locally. The three duplicated `cargo test --test …`
steps are gone from ci.yml — still run, still blocking, just no longer a second
copy of the list. `cargo audit` stays a raw step: it is `continue-on-error` and
is deliberately not part of the local gate.

Three invariants in `harness_invariants.rs` keep it true (pure text tests, no
container, run in the same job):

  INV-A1  the no-Docker CI jobs must call the make targets, and must not contain
          a raw `cargo fmt` / `cargo clippy` / `cargo test` step
  INV-A2  every `make <target>` named in AGENTS.md / CLAUDE.md exists
  INV-A3  every fully-qualified repo path named in those docs exists
  INV-A3b every bare source filename they name still exists somewhere in the tree

A3b exists because A3 turned out to be nearly vacuous: the structure block writes
bare filenames (`query_filter.rs`, not `src/query_filter.rs`), so A3 validated
two paths out of a page of them and would not have noticed a rename anywhere in
that block. Found by mutating it — my first mutation was a no-op and the guard
"passed" for the wrong reason, which is the same one-notch-weak pattern the docs
warn about, in the guard written to enforce them.

All four mutations verified to fail: CI regrowing a raw clippy step; a doc naming
a nonexistent target; a doc naming a renamed file; and the A3b extractor silently
matching nothing (a coverage floor catches that).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
82738c0 does not pass `make agent-check` — `cargo fmt --check` rejects two
blocks in the invariants I had just appended. I ran the gate before committing,
but the command was

    make agent-check 2>&1 | tail -2 && git commit ...

and a pipeline's exit status is the LAST command's, so `&&` saw `tail` succeed
and committed over a failing gate. The irony is exact: a verification step that
reported success while measuring something else, in the commit that wires up the
gate meant to stop that.

Lesson for the docs, not just this file: run `make agent-check` on its own line
and check `$?`, never through a pipe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pipeline-exit-status trap cost a bad push in this very branch:
`make agent-check | tail -2 && git commit` evaluates `tail`'s status, so the
`&&` guards nothing and the commit lands over a failing gate. A commit message
is the wrong place for that — it belongs where the next agent will read it,
which is AGENTS.md and the CLAUDE.md summary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This repo already has ANTHROPIC_API_KEY in CI for the review and triage
workflows, so 'add an AI check to the gate' is a live temptation. Recording the
reasoning where someone would edit it: a gate must be deterministic, free and
offline-runnable, and every failure this was built to catch — stale engine
counts, missing make targets, renamed files, CI drift — is already caught by
plain tests. Model review stays in the advisory PR workflow, where a flaky or
costly answer blocks nobody.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@fcostaoliveira

Copy link
Copy Markdown
Collaborator Author

The one red check here is inherited from master, not caused by this PR.

Integration tests (Milvus) fails with:

milvus-minio Error pull access denied for minio/minio, repository does not exist
or may require 'docker login': denied: requested access to the resource is denied

MinIO withdrew that release tag from Docker Hub, so tests/docker-compose.test.yml on master points at an image that no longer resolves and the job dies before a single test runs. Any branch cut from master inherits it. Fix is in flight as its own PR.

Everything this PR actually touches is green — including the two jobs it rewires, which now run make agent-check-lint and make agent-check-test instead of their own copies of the commands. That is the first live confirmation that the Makefile targets are the CI gate rather than a description of it.

@fcostaoliveira
fcostaoliveira merged commit 145c74a into master Sep 23, 2026
16 checks passed
@fcostaoliveira
fcostaoliveira deleted the agent-instructions branch September 23, 2026 11:59
fcostaoliveira added a commit that referenced this pull request Sep 23, 2026
…ocument metadata) (#319)

* feat(datasets): add MS MARCO v2.1 + Cohere embed-v3 (vectors + real document metadata)

Registers msmarco-cohere-1024-{100K,1M,10M}-cosine: MS MARCO v2.1 /
TREC-RAG 2024 passages embedded with Cohere embed-english-v3.0 (1024-dim,
cosine), each carrying its source document's docid, url, title, headings,
segment text and start_char/end_char. Every other text-embedding dataset we
ship is vectors only, so this is the first corpus where filtered and
full-text search can be measured over genuine documents rather than
synthetic payloads.

Upstream (CohereLabs/msmarco-v2.1-embed-english-v3) is a 60-shard Hugging
Face dataset of 113,520,750 passages with no tarball, so the entries carry
no `link` and are built by a new `prepare-msmarco` binary
(`make prepare-msmarco`), following the `synthetic-*` precedent. It streams
only the prefix each variant needs — the 100K build is a ~200 MB ranged read
that finishes in under a minute.

Ground truth is brute-forced, not borrowed. HF ships a top-1000 per query,
but it ranks the FULL 113.5M corpus: restricted to a 1M prefix it retains
only ~9 hits per query and is plain wrong past them, since a passage ranked
1001st globally can sit in the prefix and outrank everything that survived
truncation. The 1677 TREC-DL 2021-2023 queries therefore get a genuine
brute-force top-100 over the prepared vectors, read back from the file that
was just written.

The shipped list is then an independent oracle. Every in-prefix
global-top-1k hit necessarily outranks every in-prefix passage that is not
in the global top-1k, so our ranking's head must equal those hits id for id
and cosine for cosine, and the payload at each offset must name the same
passage. That one assertion covers the brute force, the npy-row-to-jsonl-line
alignment (a one-row slip would give every passage its neighbour's metadata,
and no recall number would show it) and the cross-shard offset arithmetic.
A disagreement aborts preparation. On the 100K build it compares 2,134
ranking positions across 562 queries and agrees to within 4.6e-5 cosine.

There is deliberately no --limit: the size is a property of the dataset
name, so two runs reporting the same name uploaded the same corpus.

Also raises Milvus' hardcoded VarChar max_length from 500 to Milvus' own
maximum (65535). That value is a server-side REJECTION threshold in UTF-8
bytes, not a hint, and 500 would have refused every MS MARCO passage
(segment bodies reach ~28,600 bytes). The field-JSON builder is extracted
into a pure function so the cap is assertable without a live Milvus.

Pieces:
- src/msmarco.rs: float16 NPY header parsing, a streaming f32 NPY writer
  (the existing write_npy_vectors flattens the whole corpus in RAM, which
  1M x 1024 cannot survive), an 8-lane dot product, the block-fed top-k, and
  the verification logic. 24 unit tests, including a guard that every variant
  matches its datasets.json entry field for field.
- src/bin/prepare_msmarco.rs: I/O only — ranged/streamed HF downloads, the
  two passes, and a PREPARED.json manifest recording provenance, shards
  consumed, per-field maximum byte lengths and the verification numbers.

Verified live: prepared the 100K variant against real HF data and ran it
end-to-end on Redis 8.8.0 (100,000 docs, recall 0.8975, MRR 1.0 at ef=128,
626 QPS), with docid/url indexed as TAG, title/headings/segment as TEXT and
start_char/end_char as NUMERIC, zero indexing failures, and full-text,
numeric-range and exact-tag queries all returning correct hits.
make check, clippy -D warnings, and 165 lib + 903 binary tests are green.

NOT verified live: the Milvus max_length change. `make integration-test-milvus`
cannot start here — the compose stack fails pulling minio/minio ("pull access
denied"), unrelated to this change — so it is unit-tested only and wants a run
on a machine that can pull those images.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(msmarco): brute-force top-1000 ground truth; address first-pass review

Ground truth depth 100 -> 1000, matching the depth of the upstream top1k_*
lists this corpus is built from, so recall@k is answerable for any k up to
1000 without re-preparing.

That has a consequence worth stating plainly: a search config that does not
set `top` derives it from the ground-truth row width, so these datasets are
otherwise searched at top=1000. Measured on the 100K corpus with Redis, the
shipped redis-docker-test config goes from 626 QPS (top derived as 100) to
40.7 QPS (top derived as 1000). Documented in the README, the datasets.json
descriptions and on NEIGHBOURS itself; set `top` explicitly for a
conventional recall@10/@100 run. Preparation cost rose 29.7s -> 39.7s and
tests.jsonl 39 MB -> 79 MB for the 100K variant.

Review items addressed:

1. The cross-check coverage is now a FLOOR, not just a report. It only
   aborted when no query had a single in-prefix hit, so a changed upstream
   export or a mis-mapped offset would still print a reassuring "Verified N
   queries / M positions" with the guard effectively gone. coverage_floor()
   derives the bar from the prefix size (a quarter of what a uniform
   distribution would retain, minimum 100 positions, plus a 5% query floor);
   the 100K build clears 83/369 with 562/2134.

2. Milvus max_length is no longer an unquantified claim. Fixed the compose
   stack first: minio stopped publishing this release tag to Docker Hub, so
   `minio/minio` fails with a misleading "pull access denied ... repository
   does not exist", killing the whole Milvus job before a single test runs --
   on master too, not just here. The tag is still on quay.io, which this file
   already uses for etcd. With that, make integration-test-milvus runs: 15/15
   pass at 65535. Then an A/B on identical data, two collections differing
   only in the declared cap: identical rowCount (5000/5000), identical
   keyword INVERTED filter (100/100) and identical TEXT_MATCH analyzer result
   (4879/4879) -- the cap is a validation bound, not an allocation. The old
   500 meanwhile rejects a 676-byte body outright ("length of varchar field
   body exceeds max length"), which is the failure this change exists to fix.

3. Sized the Redis TEXT SORTABLE cost the review could not size from the
   diff. Identical index, SORTABLE the only difference:
   sortable_values_size_mb 170.4 vs 0, inverted 171.3 vs 174.1, vector index
   427.7 both. ~1.7 KB/document purely additive -> ~1.7 GB at 1M, ~17 GB at
   10M. Recorded in the README as a sizing note. NOT changed here: `text` ->
   SORTABLE affects all 58 pre-existing datasets, which is a wider blast
   radius than this PR should carry.

5. Dropped the tautological loop in the Milvus cap test (it could not fail
   independently of the assert above it) and moved the measured byte figures
   into the doc comment where they are documentation. Added
   msmarco::cap_violations(), which checks a prepared corpus's measured
   per-field maxima against the caps engines actually enforce (Milvus VarChar
   65535 for keyword/text/uuid, Elasticsearch keyword term 32766 for keyword
   only) and warns at preparation time -- so PREPARED.json's maxima are read
   by something instead of only recorded.

6. is_complete()'s doc comment now tells the truth. Every output is written
   to <name>.part and published by rename only after verification passes, so
   a directory either holds a complete cross-checked corpus or is untouched.
   An interrupted --force rebuild now also leaves the previous good corpus
   intact rather than overwriting it with a partial one.

7. ensure_finite() rejects a non-finite embedding in both passes. A NaN
   defeats the `score <= worst` fast path (every comparison with NaN is
   false) and then lands at an arbitrary rank via unwrap_or(Equal); queries
   with in-prefix hits would abort, but the 1,115 with none would publish a
   polluted head silently. A test pins that this is what would happen without
   the check. Also documented that the id tie-break is only delivered because
   blocks arrive in ascending id order, with a debug_assert so parallelising
   pass B cannot quietly change published ground truth.

Item 4 (Valkey's text->TAG degradation re-splitting on `;` inside MS MARCO
prose) is left for a separate issue as the review suggested: nothing
published today is wrong, since these entries carry no `conditions`.

Also fixes two clippy lints that only fire on the toolchain CI uses (1.98.1,
which this machine now matches): chunks_exact_to_as_chunks and
manual_checked_ops.

make check, clippy --all-targets -D warnings, 165 lib + 903 binary tests and
15/15 Milvus integration tests all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(msmarco): cover the cross-shard offset arithmetic no shipped build reaches

Shard 00 alone holds 1,760,180 passages, so both the 100K and the 1M variant
live entirely inside it. Every live run so far therefore proves nothing about
`first_id` accumulation across a shard boundary — only the 10M build crosses
one, and nobody runs that casually (55 GB on disk).

Moves the planning arithmetic out of the preparer's network loop into
`msmarco::plan_shards(shard_rows, limit)` and tests it directly: offsets
accumulate contiguously, only the last shard may be partially consumed (the
tail check in pass A depends on that), a prefix ending exactly on a boundary
does not open the next shard, and a limit past the end of the corpus is an
error rather than a quietly short corpus. One test pins that the 10M variant
is in fact the one that spans shards, so the claim stays true if the
registered sizes change.

The preparer keeps the network half — a 512-byte ranged GET per shard header,
never the multi-GB payload — and delegates the arithmetic. Behaviour is
unchanged: re-verified live on the 100K build (562 queries / 2134 positions,
max cosine delta 4.63e-5, floor 83/369).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(msmarco): Milvus cannot ingest the 1M/10M prefixes — say so

Preparing the 1M variant end-to-end surfaced this through the cap check added
in the previous commit, which is the first thing it has caught:

    ⚠ WARNING: headings (text) reaches 185752 bytes, above the Milvus VarChar
      cap of 65535 — that engine will REJECT inserts for this dataset

The data is genuine, not a decode bug. MS MARCO carries real web-page
`headings`, and a few pages have pathological ones — the worst is a single
document's table of contents at 185,752 bytes. 40 of the first 1M passages
(0.004%) are above 65535.

65535 is Milvus' OWN ceiling for a VarChar column, so this is not something a
larger declared cap can fix. It qualifies the claim made when the cap was
raised from 500: that change lets Milvus run the 100K variant, and it does,
but Milvus cannot ingest the 1M or 10M ones at all. Every other engine takes
the corpus unchanged (Redis/Valkey/Dragonfly TEXT, Elasticsearch/OpenSearch
analyzed `text`, Weaviate, pgvector, MongoDB all have no comparable limit;
the `keyword` fields docid/url peak at 43 and 987 bytes, well inside the
32766-byte Elasticsearch term limit).

Recorded rather than left to a console warning that scrolls away:
`engine_cap_violations` in PREPARED.json, the per-variant descriptions in
datasets.json, a README paragraph, and the VARCHAR_MAX_LENGTH doc comment.

1M build verified otherwise: 1,505 queries / 17,523 ranking positions
cross-checked against the shipped global top-1k, max cosine delta 4.85e-5, 82
tie reorderings, against a floor of 83/3693. 330s to prepare, 5.4 GB on disk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(msmarco): publish prepared 100K/1M corpora to S3 and register their links

Generated on an i7i.metal-24xl (96 cores) and published to the usual
benchmarks.redislabs bucket, so these now auto-download like every other
dataset instead of requiring the preparer:

  vecsim/msmarco-cohere-1024/msmarco-cohere-1024-100K-cosine.tgz   288 MiB
  vecsim/msmarco-cohere-1024/msmarco-cohere-1024-1M-cosine.tgz    2.51 GiB

Both carry the same per-object `public-read` ACL the other datasets in that
bucket use (the bucket has no policy and no public-access block — per-object
ACLs are the established mechanism there, not something invented here).

**Reproducibility is verified, not asserted.** The 1M corpus generated on the
metal host has byte-identical SHA-256 sums for vectors.npy, payloads.jsonl and
tests.jsonl to the one generated on a 16-core workstation, and the 100K corpus
round-trips through tar -> S3 -> download -> extract byte-identically as well.
Different core counts mean different rayon thread counts, so this is the
evidence that the 8-lane dot product and the id tie-break really do make ground
truth independent of how many threads ran — the property the corpus-identity
claim rests on. The host run also reproduced the cross-check numbers exactly
(1505 queries / 17523 positions, max cosine delta 4.85e-5, 82 tie reorderings).

The drift guard's "must have no link" assertion is replaced by a stronger one
now that links exist: a link must end in `/<dataset name>.tgz` and sit under
the published prefix. An entry pointing at another size's artifact is exactly
this repo's silent-corpus-identity failure — the row-count gate would reject a
corpus that is fine, and a same-length mismatch would pass unnoticed.
Mutation-checked by pointing the 1M entry at the 10M tarball.

Adds `experiments/configurations/redis-msmarco.json` (M=32, EF_CONSTRUCTION=256,
an EF sweep at top=10 plus one at top=100). It sets `top` explicitly, which is
the point: without it these datasets derive top=1000 from their ground-truth
width. Measured with it on the 1M corpus against Redis 8.8.0 on the i7i:

  top=10  EF=64   6081 QPS  recall 0.9239  MRR 0.9922  p50 1.29ms
  top=10  EF=128  4728 QPS  recall 0.9572  MRR 0.9970  p50 1.67ms
  top=100 EF=256  3080 QPS  recall 0.9570  MRR 1.0000

Upload was 192.5s for 1M passages (5194 docs/s) including 1.5 GB of payload
text. The end-to-end download path is verified too: with the local corpus
removed, the benchmark fetched the 100K tarball from the registered link,
extracted it and ran to recall 0.9793 at top=10/EF=128.

10M is still generating on the same host and will be published separately; it
is the first run to exercise the multi-shard path (6 shards, 488,132 rows taken
from shard 5), with cross-check coverage of 1675/1677 queries and 197,898
ranking positions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(msmarco): flattened-ef guard, a real ordering check, pinned upstream revision

Second-pass review items 1-4.

**1. The `ef` sweep really is flattened at derived `top` — measured.** Ground
truth 1000 wide means a config without `top` searches at k=1000, and HNSW
raises effective search breadth to at least k, so any declared ef below 1000
never takes effect. On the prepared 100K corpus, same three ef values:

    top derived (=1000):  ef 64 -> 0.9419 @ 45.1 QPS
                          ef 128 -> 0.9419 @ 45.7 QPS
                          ef 512 -> 0.9419 @ 45.5 QPS      <- one measurement, three names
    top set (=10):        ef 64 -> 0.9514 @ 3536 QPS
                          ef 128 -> 0.9794 @ 2195 QPS
                          ef 256 -> 0.9911 @ 1576 QPS      <- a real Pareto curve

Identical recall to four decimals across an 8x ef range is not a QPS surprise,
it is a sweep publishing one point under several config names. Prose in the
README was not a guard, so there is now a real one: before the search phase,
any config that omits `top` and declares an `ef` below the ground-truth row
width is named in a warning that says what will happen and why. Verified to
fire on a derived-top sweep (naming all three configs) and to stay silent for
redis-msmarco.json, which sets `top`.

It warns rather than aborts: the behaviour is inferred from one engine's
measurement, and the run is redundant rather than wrong.

**2. The ordering guard was a `debug_assert!`, i.e. absent where it mattered.**
There is no `[profile.release]`, CI runs every suite `--release`, and the
preparer is documented as `cargo run --release` — so the assertion was compiled
out of exactly the binary that writes published ground truth, while a doc
comment three lines away claimed it protected the tie-break. Now a returned
`Err`, pinned by a test that feeds `add_block` a descending `first_id`, and the
suite is run under both profiles to prove the guard survives `--release`.

This is the "guard one notch weaker than the bug it names" shape, caught in my
own code by the reviewer rather than by me.

**3. Upstream was fetched from `/resolve/main/`.** The whole corpus-identity
claim — that a dataset name denotes fixed bytes — held only for the passage
count against a moving ref; a re-export would have silently redefined what the
name means, with nothing in PREPARED.json recording which export a directory
came from. Now pinned to commit e78737fe, asserted by a test that rejects any
`/resolve/main/` URL and requires a 40-char sha.

**4. The coverage floor fired after the download and the brute force.** Both of
its inputs are bounded by numbers `load_queries` already computes, so the same
floor now runs as an upper bound before a single shard is fetched. On the 10M
build that is seconds instead of 54 GB and ~20 minutes. Tested to agree with
the post-hoc check on the real 100K and 10M numbers.

Also drops a tautological assertion I had added to the release-profile test
(`cfg!(x) || !cfg!(x)`) — the release run is the evidence, not the assert.

10M prepared and verified on the i7i meanwhile, exercising the multi-shard path
for the first time: 6 shards, 1675 queries / 197,898 ranking positions
cross-checked (floor 83/36,931), max cosine delta 5.96e-5, 1189s, 54 GB. Its
`headings` maximum is 531,861 bytes — 8x Milvus' ceiling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(msmarco): add uniformly-sampled (crc32) twins of the three variants

The prefix variants take the first N passages, and the corpus is docid-ordered
(which tracks URL), so they are alphabetically bounded slices: the 100K prefix
spans `0-60.reviews` to `acqnotes.com`, and `url` contains "nih" zero times in
it. This adds `-crc32-` twins that keep every passage whose docid hashes below
a threshold — uniform over the whole 113.5M-passage corpus.

**Measured before building anything.** The obvious worry is that the prefix is
geometrically skewed too, which would undermine the recall numbers already
published. It is not (20k-row blocks, cosine on unit vectors):

    shard 00 head (what the prefix takes)  nn=0.8825  rand=0.1442
    shard 29 middle                        nn=0.9117  rand=0.3090
    shard 59 head                          nn=0.9316  rand=0.3699
    spread across all 60 shards            nn=0.8759  rand=0.1499

The prefix is indistinguishable from a corpus-wide spread sample; the LATER
shards are the outliers. So the prefix variants stand for pure-KNN recall and
the sampled ones are additions, not replacements — the skew is confined to the
payload distribution, which is what they fix.

`zlib.crc32(docid) % N` is the selection the Redis Enterprise MS MARCO suite
uses, so picking the same function makes the two benchmarks' corpora comparable
by construction. `crc32_bucket` is pinned bit-identical to zlib by tests using
vectors taken from zlib itself; drift there would silently decouple them.

Thresholds come from a real scan, not arithmetic: `--discover-crc32` streams all
60 metadata shards (never the embeddings) and histograms the buckets, so one
pass sizes every possible threshold via prefix sums. It counted exactly
113,520,750 passages, matching the dataset card. 886 / 8805 / 88075 realize
99,964 / 1,000,044 / 9,999,959 and nest, so each sampled variant is a strict
subset of the larger ones.

A hash threshold cannot land on a round number, so the round number lives only
in the name and `vector_count` carries the realized count. Preparation
hard-errors if the two ever disagree — that is how a changed upstream export
would announce itself. Paths are named by THRESHOLD (`crc32-t8805`), not by a
round size: `1M-crc32` would advertise 1,000,000 points the corpus does not
have, which config.rs's #224 guard rejects on precisely the reasoning that
makes it wrong. That guard caught this naming; the fix is the one its own doc
comment prescribes.

Mechanics the sampled path needed:

* `DeferredNpyF32Writer` — the row count is unknowable until the last shard is
  read, so it reserves the header, streams, then seeks back and patches it. A
  test pins that every count from 0 to 113,520,750 renders to the same 128-byte
  header, which is the one way the design could silently shift every vector.
* Concurrent shard reads. A single CDN stream measured ~12 MB/s, 16 measured
  265-325 MB/s — a 22x difference that turns a ~2h scan into ~2 minutes. Shards
  write per-shard temp files and are concatenated IN SHARD ORDER, because local
  ids must follow corpus order: `TopK::add_block` requires ascending ids and the
  ids are what ground truth is keyed by.
* Shipped top-1k offsets are remapped through the selection to local ids. The
  oracle argument never depended on the subset being a prefix, only on its being
  a subset, so it carries over unchanged.

Six variants registered now, three per family. 44 lib tests + 903 binary tests
green in both debug and release, clippy -D warnings clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(msmarco): measured metadata coverage, prefix vs uniform

A 100K uniform sample spans the whole alphabet (35 distinct leading host
characters, 1000naturalremedy.com to zyto.com) while the 100K prefix covers 11
(0-9 and 'a', stopping at acqnotes.com) — and even the 10M PREFIX only reaches
'w'. Concretely: 'nih' appears in 173 urls of the uniform 100K and 0 of the
prefix 100K; 'wikipedia' in 6,401 vs 0.

That is the whole case for the -crc32- twins, so it belongs in the README as
numbers rather than as an assertion that the prefix is 'skewed'.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(msmarco): review items 5-8 — verify-in-place, 10x tighter oracle, digest pin

**5. The cosine tolerance was ~40x the largest disagreement any real build
produces**, and it doubled as the tie window, so a differing id was accepted as
a tie anywhere inside it. Settling that needed a way to re-run the cross-check
without rebuilding, so `--verify` now does exactly that: everything the check
needs is already on disk, and it matches shipped hits by DOCID rather than by
offset, which makes it work for both sampling modes and independent of the
offset arithmetic the build used. It reproduces a build's numbers exactly
(562 queries / 2,134 positions / 4.63e-5 on the 100K), which is itself a check.

Measured across all three corpora at decreasing tolerances:

    corpus  positions  max delta  ties@2e-3  @2e-4  @1e-4  @5e-5
    100K      2,134    4.63e-5        0        0      0      0
    1M       17,523    4.85e-5       82       82     82     82
    10M     197,898    5.96e-5    5,406    5,406  5,406      -

The counts do not move as the window shrinks 40x, so every tie is genuine and
nothing was being waved through. Tolerance is now 2e-4 — 10x stricter, still
~3.4x headroom over the worst observed delta.

`--verify` is worth having on its own: it re-validates a downloaded corpus in
minutes rather than re-deriving it.

**6. minio pinned by digest.** The failure this replaced was a tag moving out
from under us; a tag on a second registry is the same promise from a different
party. `quay.io/minio/minio@sha256:14cea493...` makes the pin mean one immutable
image, verified to pull.

**8. `publish()` now has a completion marker written last**, required by
`is_complete`. Four renames are four operations: dying between the second and
the fourth left a new PREPARED.json + tests.jsonl over an old payloads.jsonl +
vectors.npy, which the previous "three files exist and the row count matches"
check accepted. Note this means a corpus prepared before this change (or
extracted from an already-published tarball) has no marker and will be rebuilt
rather than skipped — it fails safe, toward re-deriving.

Item 7 (the RAM figures) is left for a measured number rather than a bigger
estimate; peak RSS will be captured from the 10M Redis run.

Also: 1M-crc32 built and verified on the fleet host — 1,000,044 of 113,520,750
selected (0.8809%), and 1,676 of 1,677 queries retain hits against the prefix
1M's 1,505. The uniform sample keeps giving broader oracle coverage because it
touches the whole corpus instead of one stretch of the alphabet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(msmarco): publish and register the 1M uniform corpus

msmarco-cohere-1024-1M-crc32-cosine.tgz (2.8 GiB) uploaded with the same
per-object public-read ACL the bucket's other datasets use, anonymously
reachable, link registered. 1,000,044 passages selected from all 113,520,750
(0.8809%); 1,676 of 1,677 queries retain shipped top-1k hits, against 1,505 for
the same-size prefix — the uniform sample keeps buying broader oracle coverage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(msmarco): register the 10M uniform corpus

10M-crc32 built and verified on the fleet host: 9,999,959 of 113,520,750
passages selected, and the cross-check reached ALL 1,677 queries across 147,422
ranking positions — the broadest oracle coverage of any variant, and the only
one where no query goes unchecked. Max cosine delta 5.14e-5, 2,448 tie
reorderings, 1212s.

Note the shape of the prefix-vs-uniform difference across sizes: the prefix 10M
compares MORE positions (197,898) but across FEWER queries (1,675), because its
hits concentrate in one stretch of the alphabet. The uniform variants trade
depth per query for breadth across queries, which is the better property for an
oracle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(msmarco): oracle coverage across all six corpora

The cross-check can only speak for a query that retains a shipped top-1k hit,
so coverage-in-queries is the number that matters, and it is where the two
families differ most:

  prefix  100K  562/1677    uniform 100K  1005/1677
  prefix  1M   1505/1677    uniform 1M    1676/1677
  prefix  10M  1675/1677    uniform 10M   1677/1677

At equal size the uniform variants reach far more queries because their hits
spread across the corpus instead of concentrating in one stretch of the
alphabet. They compare fewer positions in total but across more queries — the
better trade for an oracle. The uniform 10M is the only variant where no query
goes unchecked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(msmarco): third-pass review — a real regression, a half-blind guard, wrong figures

**1. `9aa5d38` broke the prefix path and I did not notice, because every live
prefix build in this PR predates it.** `wanted_offsets` was widened to the full
shipped top-1k (~1.6M offsets) for the sampled path, and the prefix branch was
handed the same unfiltered set. `write_corpus` can only record a docid for a row
it writes, so its completeness check fired on a perfectly correct corpus:

    prepare-msmarco: error: 1592762 of the 1594767 offsets named by the shipped
    top-1k were never given a docid while writing the corpus — the offset
    arithmetic is wrong

A confident and entirely wrong diagnosis. Reproduced, fixed, and re-verified:
the 100K prefix now builds and is byte-identical (all three SHA-256s) to the
published artifact, so no shipped corpus was ever affected.

The fix is `msmarco::offsets_to_track(shipped, sampling, limit)` — in the
library, under test, with the two modes' answers asserted to differ. The
reviewer's diagnosis of WHY it escaped is the useful part: "logic in msmarco.rs,
binary is I/O only" is the convention, and this was offset-selection logic that
ended up in the binary with no test to mutate.

**2. The flattened-ef guard only understood one engine family's knob.** It read
the typed `ef`, but pgvector and qdrant use `hnsw_ef` and elasticsearch, vertex
and mongodb use `num_candidates` — both in the untyped `extra` catch-all, which
a typed read cannot see. Those families flattened silently, which is the exact
state the warning exists to name. It now resolves all three spellings.

It also fired where its message was false: `redis-single-node.json` carries two
`ef: 64` entries per config differing only in `parallel`, so against a 100-wide
dataset it printed 12 warnings claiming "several names, one measurement" about a
single sub-width value. Now requires TWO OR MORE DISTINCT sub-width values.
Verified both ways: fires on the real 3-value flattening, silent on
redis-single-node + random-100.

**6. "Comparable by construction" with the RS suite was overstated.** Same hash,
different modulus: 100 divides 1,000,000 and buckets 0..885 span every residue
mod 100, so our 886/1e6 sample and their 1/100 sample overlap in ~1% of our
passages and neither nests in the other. What is shared is an auditable
selection rule, not a document set. Corrected in the README and all three
descriptions.

**7. Three figures that did not multiply out.** Measured from the HF tree API:
`passages_jsonl` is 26.9 GB gzipped (not ~100 GB) and `passages_npy` is 232.5 GB,
so a full build scans 259 GB, not ~330 GB. Both numbers corrected everywhere
they appear.

**3, 4, 8, 9.** AGENTS.md and the binary's module doc still said the msmarco
entries have no `link` — all six now do, and all six are anonymously reachable
(verified 200). The `TopK` doc still justified its sorted-Vec on "k is 100"
after NEIGHBOURS became 1000. The flattened-ef comment carried a QPS triple from
a different run than the commit body quoted; both are real, the throughput is
noise at that k, and the comment now says so rather than picking one.

Item 5 is resolved by events: all three crc32 corpora are now built, verified
and published. Item 10 (minio digest pin) landed in 0cbc436.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(msmarco): measured 10M Redis results and peak RSS (review item 7)

Ran the 10M prefix corpus against Redis 8.8.0 on the i7i with
/usr/bin/time -v, so item 7's RAM figure is measured rather than argued:

    top=10  EF=64   5507 QPS  recall 0.9073  MRR 0.9672
    top=10  EF=128  4242 QPS  recall 0.9364  MRR 0.9773
    top=10  EF=256  2797 QPS  recall 0.9437  MRR 0.9905

    upload   3399 s (2942 docs/s, about half the 1M rate — HNSW insert cost
             grows with graph size)
    wall     58m32s
    peak RSS 80,485,728 KB = 80.5 GB

**The measurement contradicts the review's reasoning, and agrees with the
number.** The concern was that the README's "~80 GB" counted only the vector
half and that payloads-as-MetadataItem would push the real ceiling well above
it. It does not: the high-water mark is the vector read alone, where
`read_npy_vectors` holds the `Array2` and the `Vec<Vec<f32>>` it is converting
into at once — 2 x 40.96 GB. The 16 GB of payloads are read *after* that
`Array2` is dropped, so the two peaks never coincide.

So the figure stays ~80 GB but the README no longer implies it is the vector
half of a larger total; it now says what the peak actually is and why, which is
the part someone sizing a machine needs. Rule of thumb: 2x the vector bytes,
plus Redis in its own process.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: make the repo agent-friendly (Claude + Codex), and fix what was stale

`AGENTS.md` still opened with "Supports Redis (RediSearch) and VectorSets
engines" and "only Redis and VectorSets are ported" — the repo has **15**, and
its own table four lines below listed eleven. An agent reading that starts from
a false model of the codebase, so this is a correctness fix, not tidying.

**`make agent-check`** — the highest-leverage addition. It reproduces the entire
no-Docker CI gate in one command, in CI's order and profile:

    cargo fmt --check
    cargo clippy --all-targets -- -D warnings
    cargo test --lib --bins --release
    cargo test --test {integration_cli,overhead_invariants,harness_invariants} --release

This exists because `make check` is `fmt-check` + `lint` and **runs no tests**,
while Workflow Rule 1 said "always run `make check`" — so following the
instructions exactly produced a green local run that CI could fail. Both facts
are now stated where they will be read, including in `make help`.

`--release` is called out explicitly: CI runs the suites in release, so a
`debug_assert!` does not exist there. That is not hypothetical — a guard added
earlier in this branch was compiled out of precisely the binary that writes
published ground truth.

**Four engines had integration tests but no make target** (dragonfly, kividb,
chroma, vertex) while Rule 5 says "never bypass make targets" — the rules made
those suites unrunnable. Added, matching CI's ports and env vars; vertex is
listed explicitly as cloud-only so its absence from the Docker targets is a
documented fact rather than an oversight.

Also: refreshed the structure block (it predated the sparse/multivector readers,
`msmarco.rs`, `query_filter.rs`, `prepare-msmarco` and the invariant suites),
completed the engine table, and added a "Traps that have actually cost time here"
section — each entry a real incident, each one invisible while you are doing it.

`CLAUDE.md` was a single line reading `AGENTS.md`. It now points there explicitly
and carries the four things worth knowing if nothing else gets read, so Claude
and Codex work from one set of instructions that cannot drift apart.

Verified: every make target and every source path named in AGENTS.md exists, and
`make agent-check` passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Revert "docs: make the repo agent-friendly (Claude + Codex), and fix what was stale"

This reverts 8e84d53. The agent-instructions work is unrelated to the MS MARCO
datasets — it touches no dataset, engine or benchmark code — and has moved to
its own PR (#320) branched from master.

Reverted rather than dropped from history because this repo does not force-push.
Net effect on this PR's diff: AGENTS.md and the Makefile now carry only the MS
MARCO section and the `prepare-msmarco` target, which do belong here; CLAUDE.md
is back to matching master exactly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant