Skip to content

feat: ADR-280/281/282 — durable RVF metadata, role-aware embeddings, nightly research quality gate - #774

Merged
ruvnet merged 19 commits into
mainfrom
feat/implement-adr-273-275
Aug 3, 2026
Merged

feat: ADR-280/281/282 — durable RVF metadata, role-aware embeddings, nightly research quality gate#774
ruvnet merged 19 commits into
mainfrom
feat/implement-adr-273-275

Conversation

@ruvnet

@ruvnet ruvnet commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Implements three ADRs, hardened across five rounds of adversarial review.

ADR-280 — RVF durable self-contained metadata

Metadata is now persisted in the container with delta-encoded generations (full snapshot every 32 deltas). The first implementation wrote a full snapshot per commit and replayed every historical one at open — 600 commits produced a 725 MiB file that could no longer be opened, with no repair path. Now: 241 KB of META payload for the same workload, opens in ~4 ms, compact() collapses to a single segment.

Also fixed during review: derive-child containers that could not be reopened; an 80-byte crafted file that drove a 512 MiB allocation; delete() rollback that left in-memory tombstones behind and bricked the artifact; acknowledged writes lost because ten BufWriter sites discarded flush errors before sync_all; corrupt mid-chain deltas making a file unopenable (now recovers the longest valid prefix); and an ordering bug where recovery pruning was committed without its re-anchoring snapshot, so rvf ingest printed a repair warning and then destroyed the file.

ADR-281 — Role-aware embedding APIs

Query/passage role routing with attested embedding-space identity. Review found the space id hashed CARGO_PKG_VERSION, so a routine version bump would have rejected every persisted corpus and invalidated every cache key — with the test suite structurally blind to it (fixtures used a literal version). Now keyed on a dedicated format revision, with a golden-id test pinning the value and a blindness test proving version changes don't move it.

Also: three constructors that failed unconditionally with ten callers never migrated (the worker daemon could not start); prompt templates now applied from the attested identity rather than hardcoded strings, fail-closed on mismatch; the identity check moved into the vector store's own config table so deleting a sidecar no longer downgrades it; ApiEmbedding (a default feature) no longer bypasses templating.

ADR-282 — Nightly research quality gate

Sandboxed candidate evaluation with attested evidence. Review found the gate had never completed a single run: the candidate checkout was shallow so its git diff always failed, and a jq quoting bug made the authorized-override path dead code. Check-run queries were also unpaginated — on a real main commit, 8 of 22 failures (including Build, Test, cargo-audit) were invisible, so a red base could be certified green.

Now: schemas are load-bearing (offline registry, pinned + fully hashed dependency closure, tamper-tested), the evidence root must match the attested index exactly, deletions are scanned, and report headlines are re-bound to the trusted evaluation.

Verification

~1,100 Rust tests across both workspaces, 25 Python gate tests, cargo check --workspace --all-targets clean. Review verdict: SHIP, with both new regression tests confirmed load-bearing by mutation testing.

Follow-ups filed, none blocking: #770, #771, #772.

🤖 Generated with claude-flow

ruvnet added 19 commits July 29, 2026 18:13
…3-275

# Conflicts:
#	npm/package-lock.json
#	npm/packages/ruvector/package.json
…beddings, nightly research quality gate

- ADR-280: RVF durable self-contained metadata (rvf-types metadata.rs,
  runtime options/store/filter/safety_net/vector_slab wiring, crash-safety
  and durability integration tests)
- ADR-281: role-aware embedding APIs in ruvector-core (embeddings.rs,
  agenticdb.rs, error taxonomy across core/hailo/hailo-cluster, role test)
- ADR-282: nightly research quality gate (research-gate scripts + schemas,
  research-* GitHub workflows, CODEOWNERS, sota-bench metaharness harness
  with scorePolicy + darwin.ts, gate override + promote pipelines)

ADRs renumbered from 273-275 to 280-282 after PR #752 took 273-279.

Co-Authored-By: claude-flow <ruv@ruv.net>
… overshoot

- cache_hits_skip_transport_after_first_call: capacity 16 gives per-shard
  capacity 1 (16 shards), so distinct texts whose keys hash to one shard
  evict each other — the embedding_space_id cache-key change surfaced
  this. Capacity 32 makes the test independent of hash layout.
- ttl_insert_refreshes_timestamp: 20ms TTL with a 5ms margin fails
  deterministically on macOS where thread::sleep overshoots; widened to
  100ms TTL with ~30ms margins. Pre-existing on main.

Co-Authored-By: claude-flow <ruv@ruv.net>
…ild repair, allocation clamp, durability ordering

- Metadata commits now emit generation deltas against the previous
  committed state (full snapshot on generation 1 and every 32 deltas),
  replacing full-snapshot-per-commit which made artifacts permanently
  unopenable past the replay ceiling (600 commits: META payload 5.4MB
  -> 241KB, reopen 10.2ms, compact() -> single 127KB META segment)
- restore_metadata walks newest-first to the nearest full snapshot;
  tolerates corrupt superseded generations and torn newest (falls back)
- compact() rewrites one fresh snapshot instead of preserving stale
  META segments (which could reference removed vectors)
- derive() children no longer persist parent metadata for vectors they
  cannot resolve; child metadata_generation resets to 0
- rvf-types metadata decode clamps Vec preallocation to what remaining
  input can hold (80-byte blob claiming 16.7M records no longer drives
  a 512MB allocation)
- delete/set_file_metadata now fsync payload before manifest (matching
  ingest); compact propagates dir-fsync errors
- crash tests extended to delete/set_file_metadata paths; fixed the
  crash-test segment walker which was finding no boundaries at all
- fix rvf-launch compile break from SearchResult.metadata field

Co-Authored-By: claude-flow <ruv@ruv.net>
…late fidelity, working legacy constructors

- Legacy constructors (LatticeEmbedding::from_pretrained/with_model,
  HailoEmbedder::open, HailoClusterEmbedder::new) derive identities from
  the model registry instead of failing unconditionally — worker daemon
  and all ten unmigrated call sites work again; hailo overwrites registry
  hash defaults with digests of on-disk artifacts
- Prompt templates now come from the attested identity (verified against
  prompt_template_sha256) instead of hardcoded query:/passage: strings;
  PrefixPolicy::Custom and hash mismatches fail closed
- AgenticDB persists the embedding-space identity and enforces
  EmbeddingSpaceAccess::for_identities on reopen — same-dimension
  different-model corpora now error with EmbeddingSpaceMismatch instead
  of silently mixing spaces; missing stored identity adopts-with-warning
  so pre-ADR data still opens
- Artifact provenance verification runs in every build and fails closed
  on unrecognized layouts or missing declared artifacts
- Model registry rewritten as pinned per-model entries; adds
  all-mpnet-base-v2, multilingual-e5-large, paraphrase-multilingual-
  MiniLM-L12-v2 and missing e5 aliases

Co-Authored-By: claude-flow <ruv@ruv.net>
…ly run, and fails closed

- Candidate checkout diff no longer dies on shallow clone: base commit
  is fetched from the sibling base checkout on disk (no network); the
  gate had never completed a single run
- Fixed jq quoting that made the authorized-red override branch dead
  code (exit 3 on every invocation)
- Check-run/status queries paginate and cross-check total_count, fail
  closed on mismatch — on a real main commit 8 of 22 failures (Build,
  Test, cargo-audit among them) were invisible to the old single-page
  query and a red base could be certified green
- Schemas are now load-bearing: offline-registry jsonschema validation
  (pinned jsonschema==4.23.0) runs before the hand-rolled checks in
  every gate validator; report.json is schema-validated and its
  headline values re-bound to the trusted evaluation (ADR-282
  acceptance criterion 13, previously unimplemented)
- Override requires approver != dispatcher; promote workflow moves
  html_url into env; deletions now included in preflight scan (a
  candidate could previously delete Cargo.lock undetected); pycache
  untracked and ignored

Co-Authored-By: claude-flow <ruv@ruv.net>
Seconds-scale TTL with an elapsed-time guard: the hit is only asserted
when get() is reached within the TTL of the refresh, so scheduler
stalls skip rather than false-fail. The 100ms version still flaked
under parallel cargo builds.

Co-Authored-By: claude-flow <ruv@ruv.net>
…isted-view deltas, chain recovery, flush errors

- delete() snapshots and restores deletion_bitmap, membership_filter,
  metadata, chain state, segment_dir, epoch, and witness hash as one
  unit — a failed delete can no longer publish tombstones for records
  the META chain still carries (previously bricked the artifact)
- Delta bases now diff against the persisted META chain view
  (committed_metadata), not in-memory state — records that become
  committable after being filtered are persisted in the next generation
  instead of silently lost; base advances only after manifest fsync
- restore_metadata recovers the longest valid prefix of a damaged
  chain instead of failing (up to 32 single points of failure before);
  new RvfStore::metadata_recovery() reports served generation and
  dropped ones
- Explicit BufWriter flush at all ten block-scoped writer sites —
  drop was discarding flush errors before sync_all (acknowledged
  writes lost near-full volume; reproduced)
- Write-time chain budget: MetadataChainState forces a snapshot when
  the decoded-bytes budget would be exceeded and refuses commits that
  open could not replay
- delete() error paths now roll back like ingest_batch

Each regression test verified to fail with only its fix reverted.

Co-Authored-By: claude-flow <ruv@ruv.net>
…per-proof identity storage, closed template bypasses

- embedding_space_id no longer hashes the crate version: dedicated
  EMBEDDING_SPACE_FORMAT_REVISION (bump only on semantic change)
  replaces CARGO_PKG_VERSION in runtime_revision — a routine release
  bump no longer bricks every persisted corpus and cache key. Golden-id
  test pins the current id; blindness test proves version changes leave
  ids byte-identical while template changes move them
- Identity persists in the vector store's own config table
  (authoritative) with the .agentic sidecar as a healed mirror —
  deleting the sidecar no longer downgrades the space check; populated
  guard consults the actual vector store
- ApiEmbedding (default feature) now applies the identity's prompt
  template and fails closed at construction on Custom/tampered
  policies — same guarantee as the ONNX path, proven against a local
  capture server
- Removed hashed hardcoded ort version string (was both stale and an
  id-instability vector); provider-class revision + informational
  build_revision() accessor instead (wire schema unchanged: 17 fields)
- Lattice: fail-closed assertion that lattice-embed's internal prompts
  match the registry templates the identity attests (it owns prefixing;
  double-prefixing avoided)
- Hailo: artifact_digests() helper so attested identities can verify;
  fallback identity keys off artifact digest, not directory basename
- EmbeddingCache: documented small-capacity shard degeneracy

Co-Authored-By: claude-flow <ruv@ruv.net>
…on, hashed dependency closure, exact evidence set

- actions/setup-python (SHA-pinned v5.6.0, python 3.12) before all five
  pip installs — avoids PEP 668 externally-managed failures on
  ubuntu-24.04 and makes the trusted interpreter deterministic
- requirements.in declares both direct imports (jsonschema,
  referencing); requirements.txt is a uv-compiled fully-hashed
  universal lock (126 hashes incl. platform wheels); installs use
  --require-hashes (tamper-tested: zeroed hash → pip refuses)
- validate_artifact_index requires the evidence root to equal the
  indexed set exactly — unindexed files can no longer ride into the
  attested bundle; symlinked dirs reported by name, never followed

Co-Authored-By: claude-flow <ruv@ruv.net>
…opped deletes

- After a truncated recovery, restore_metadata retains only the applied
  META entries in segment_dir and sets needs_snapshot, so the next write
  re-anchors the chain with a full snapshot. Previously every write after
  a recovery was silently discarded forever (the forward-apply loop could
  never reach a delta appended past orphaned newer generations) and only
  compact() repaired it, losing everything written in between.
- Post-replay validation now distinguishes intact from truncated chains:
  on an intact chain a deleted/absent vector for a restored record is
  still a hard InvalidMetadata, but on a truncated chain it is an expected
  consequence of the damage (deletion state comes from the newest
  manifest, metadata from an older prefix) — those records are dropped and
  counted in MetadataRecovery::dropped_records instead of bricking the
  artifact. Same hole fixed in restore_cow_state.
- Recovery is now visible: rvf-cli warns on stderr at all nine open sites
  naming the recovered generation, counts, and the compact remedy;
  RvfStore::open documents that a successful open does not imply all
  committed metadata is present.

Regression tests now WRITE after recovering — read-only reopen is what
let both defects through review.

Co-Authored-By: claude-flow <ruv@ruv.net>
Round-4's recovery fix split one operation across two writers: recovery
pruned orphaned META entries from segment_dir (persisted by
write_manifest) while only write_metadata_generation consumed
needs_snapshot. Any mutation that rewrote the manifest without emitting a
metadata generation — ingest_batch(.., None), freeze(), the embed_*
paths — committed the pruning alone, so the next open saw no damaged
generation, misclassified the truncated chain as intact, and hard-errored
forever with no repair path (compact was unreachable because open itself
failed). rvf-cli printed the 'run rvf compact' warning and then destroyed
the artifact five lines later.

Recovery now records unreachable META offsets without touching
segment_dir; write_metadata_generation drops them in the same call that
emits the re-anchoring snapshot. A manifest-only write publishes neither
half, so the chain stays byte-identical and the next open recovers
identically.

Also: delete/set_file_metadata rollback snapshots the full segment
directory (truncate could not restore middle removals once pruning
existed); dropped_generations counts every lost generation instead of
stopping at one; the recovery warning is wired into all 14 rvf-cli open
sites (freeze and the embed_* commands — the ones that triggered the
brick — had none).

Tests: an intact-chain control pinning the fatal-inconsistency check
(nulling it previously left every test green), and a manifest-only-write
after truncated recovery covering both ingest_batch(.., None) and
freeze().

Co-Authored-By: claude-flow <ruv@ruv.net>
…ness

harness/test/research.test.ts shells out to research_gate.py, which now
schema-validates its inputs (ADR-282 hardening) and therefore needs
jsonschema + referencing. The research-* workflows install them; this one
never did, so the harness job failed with ModuleNotFoundError. Uses the
same SHA-pinned setup-python and the same --require-hashes lock.

Also picks up a rustfmt miss in metadata_durability.rs.

Co-Authored-By: claude-flow <ruv@ruv.net>
…mory ids (2.6.2)

The JS wrapper read snake_case fields from the napi-rs native module, but
napi-rs camelCases Rust struct fields on the way out — so query() silently
dropped requestId, contextSize and latencyMs, route() dropped contextSize
and topP, and stats() dropped every field. The type declarations also
described the native shape as snake_case, which is why TypeScript never
caught it. Both spellings are now accepted, camelCase first.

Also:
- searchMemory: native reports `distance` (smaller is closer) but
  MemoryResult documents `score` as a similarity — convert rather than
  passing a distance under a similarity's name.
- similarity(): f32 accumulation returns 1.0000001 for an identical pair,
  which makes Math.acos(sim) NaN downstream. Clamp to [-1, 1].
- addMemory/MemoryResult.id: native returns UUID strings now; the public
  type said number. Widened to a documented MemoryId = string | number and
  updated the test that encoded the old assumption.

Carries the previously unpublished ESM fix from bb36eb1 (2.6.1 was never
released). 110/110 tests pass; esm smoke and extension verification green.

Co-Authored-By: claude-flow <ruv@ruv.net>
26 packages had npm ahead of main's source — main declared versions that
were never what users install, because packages were published from
branches that did not merge back. Version fields now match the registry.

This aligns version NUMBERS only. At least @ruvector/rvf-node has real
content drift too: published 0.2.3 ships index.js at 9191 bytes with a
different digest than main's 4510-byte copy, and declares a different
`files` list. Version alignment makes the drift visible rather than
hiding it behind a stale number; reconciling the source is separate work
tracked in its own issue.

All 26 edits touch only the version line (verified: 26 files, 26
insertions, 26 deletions). Every package.json still parses, and every
exact intra-repo dependency pin now agrees with the local version it
names — the ruvllm platform packages in particular were pinned at 2.0.1
while main declared 2.0.0.

Co-Authored-By: claude-flow <ruv@ruv.net>
…an silently returns a stale manifest

An external implementation attempt (rvQR) built a reader from ADR-009 as
the normative spec and it did not work on real files. Root cause: two
writer paths. rvf-wire/rvf-manifest emit a 4096-byte Level-0 root at the
tail and pad segments to 64-byte boundaries; rvf-runtime — the crate
behind rvf-cli, which produces the files people actually have — emits
neither, writing header and payload back to back and terminating in a
MANIFEST segment. rvf-runtime depends on neither wire crate.

The failure was worse than the spec merely being incomplete:
find_latest_manifest's 64-byte-stride backward scan cannot reach a
runtime container's newest manifest (offset 2092 % 64 = 44) but does
reach offset 0, so it returns Ok with the stale epoch-0 manifest
reporting zero vectors. A conformant reader opens the file, reports an
empty store, and raises no error.

- 2.1 describes both shapes as first-class, marks the root manifest
  optional, notes containers under 4096 bytes cannot carry one, and
  specifies a two-path reader (tail root, else byte-wise scan for the
  newest MANIFEST segment)
- 5 drops 'validate 64-byte alignment of the segment start', which
  rejected every runtime segment
- 2.4 adds rvf-runtime as a normative source and states that sources 3
  and 4 genuinely disagree, each normative for what it produces
- acceptance criterion 3 splits into 3 (wire) and 3a (runtime); adds 7
- research docs corrected: the tail-scan pseudocode in binary-layout.md
  was the failing reader verbatim

Also documents a third divergence: runtime hashes with a CRC32 rotation
labelled checksum_algo=0 while rvf-wire reads 0 as XXH3-128, so
cross-path hash verification fails.

The amended algorithm was implemented literally from the ADR text and
returns offset 2092, epoch 1, 24 vectors on a real container.

Refs #775

Co-Authored-By: claude-flow <ruv@ruv.net>
Both jobs were failing. The clippy failure is ours; the audit failure
predates this branch (red on main since at least 2026-05-16).

clippy -D warnings: the hailo-cluster-as-provider example still called
EmbeddingProvider::embed, which ADR-281 deprecated, and -D warnings makes
a deprecation fatal. Migrated its three call sites to the role-aware API
and documented why the similarity check still reads ~1.0 (the cluster
provider declares a symmetric identity, so both roles encode identically;
against an asymmetric model doc[0] would rank first but well below 1.0).

cargo audit: five new advisories. event-listener 5.4.1 is the only one
with a fix (RUSTSEC-2026-0221, unsound: StackSlot unconditionally
implements Send/Sync, letting a !Send tag cross threads) — patched in
5.4.2, so it is bumped rather than ignored, per this file's own rule that
anything fixable gets fixed. The other four carry patched = [] upstream:
pqcrypto-traits, pqcrypto-internals, proc-macro-error2 and ttf-parser,
each ignored with a justification.

Co-Authored-By: claude-flow <ruv@ruv.net>
@ruvnet
ruvnet merged commit a2326c0 into main Aug 3, 2026
75 of 77 checks passed
@ruvnet
ruvnet deleted the feat/implement-adr-273-275 branch August 3, 2026 17:13
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