tidaldb/applications/forage
jordan c97aaa8e5b fleet remediation: make the workspace gate runnable, then fix what it caught
`cargo test --workspace` could not run at all: dependency resolution failed with
"aws-types@1.3.16 requires rustc 1.91.1" on the 1.91.0 default toolchain, so the
gate the project documents was dead. Making it run exposed a compile break and
two wrong tests that had been invisible for months. Now green end to end:
143 suites, 3155 tests, exit 0.

Toolchain
- rust-toolchain.toml pins the DEV toolchain to 1.91.1. The published MSRV stays
  `rust-version = "1.91"` (the engine builds on 1.91.0); only tidalctl's AWS SDK
  chain needs the patch release, and it now declares that itself.

Consumer crates migrated to the current engine API (clean cutover)
- iknowyou-engine: `AgentPolicy` gained five m10 read/profile-override fields;
  the literal now spreads `..AgentPolicy::default()` as the engine's own doc
  example does, so future fields do not break it again.
- forage-engine: `RetrieveResult` gained p1 `reasons`. The app builds its own
  candidate pool, so it now tags what it knows: PreferenceMatch for the
  preference-vector blend, SemanticMatch (with the seed item) for
  similar-to-saved, ExplorationBudget for pinned discoveries.
- forage-engine: `url_to_item_id` folded into the u32 item universe. The engine
  narrows item IDs to a u32 slot in durable per-user state and rejects anything
  above u32::MAX rather than alias two items forever, so every add_item with a
  64-bit FNV hash failed. 9 of 28 smoke tests were failing on this alone.
- forage-engine: bridge items read the top-2 preference CLUSTERS via
  `query_vectors`, not the single centroid from `preference_vectors().get()`.
  Since m12 that accessor returns only the strongest cluster, so a tech+jazz user
  whose interests split into two clusters looked single-interest and never
  bridged. Falls back to top-2 dimensions when a user has one cluster.

Reconcile tests corrected to the shipped contract
- tidal/tests/m8p3_reconcile_production.rs asserted `3 + 5 == 8` for a windowed
  count after heal. `take_crdt_snapshot` deliberately keys signal contributions
  to ONE canonical contributor (ShardId::SINGLE) because signals are relayed from
  a single writer, so per-node attribution double-counted every replicated event
  on every reconcile. Merge is therefore LWW on (last_update_ns, score) plus
  PN-counter per-node max: nodes converge on the more complete accumulator. The
  old expectation was asserting the bug that fix removed.
- Rewrote to assert convergence, count survival (not 0), and no inflation, and
  added `repeated_reconcile_of_converged_nodes_does_not_creep` - the regression
  guard for the creep itself, which nothing covered.

Pre-commit hook unified
- hooks/pre-commit dropped `-D warnings`: each crate's `[lints]` table is the
  source of truth (`clippy::all`/`unwrap_used` deny, `pedantic` warn), and the
  flag promoted ~58 deliberate pedantic warnings in integration tests to errors,
  making every Rust commit impossible.
- It now lints all five tidal crates instead of path-matching `tidal/`, which
  silently skipped tidal-server, tidal-net, tidal-stress, tidalctl and
  applications/ - the rot above lived in exactly those crates. Ported the
  CODING_GUIDELINES file-length, println, and unsafe-SAFETY checks from the
  divergent untracked copy that this replaces.
- CONTRIBUTING.md now documents the real commands and the toolchain/MSRV split.

Fleet recovery and soak
- scripts/restore-fleet.sh: the fail-closed selective restore, promoted out of an
  ignored tmp/ directory into the repository. Preflights retained storage,
  digest-pinned images, parked state, and aggregate plus per-PV-node scheduler
  headroom before the first scale; writes a durable transcript under
  tmp/restore-logs/ with structured start/error/rollback/complete events.
- k8s manifests park the standalone store, the RF3 cluster, and the soak monitor
  at zero replicas with restore-fleet.sh as the only supported scale-up path.
- soak-eval/soak-watch and the nightly CronJob fail closed on stale or missing
  restart evidence instead of silently skipping the restart-aware half of the gate.
- docs/ops/capacity-planning.md corrects the RAM envelope to the real hot-tier
  formula and separates analytic totals from the measured process envelope.
2026-08-16 12:38:14 -06:00
..
embedder chore: reuse reqwest::Client across requests in forage embedder; minor forage updates 2026-02-23 23:16:32 -07:00
engine fleet remediation: make the workspace gate runnable, then fix what it caught 2026-08-16 12:38:14 -06:00
extension chore: reuse reqwest::Client across requests in forage embedder; minor forage updates 2026-02-23 23:16:32 -07:00
js chore: reuse reqwest::Client across requests in forage embedder; minor forage updates 2026-02-23 23:16:32 -07:00
server feat: complete M8 replication primitives + forage enhancements + docs 2026-02-24 13:17:19 -07:00
agent.md feat: complete M8 replication primitives + forage enhancements + docs 2026-02-24 13:17:19 -07:00
architecture.md feat: complete M6-M7 + Enterprise Readiness milestones; split oversized source files per CODING_GUIDELINES §9 2026-02-23 22:41:16 -07:00
plan.md feat: complete M8 replication primitives + forage enhancements + docs 2026-02-24 13:17:19 -07:00
README.md chore: doc consolidation, seven-dimension review fixes, and commit hooks 2026-06-08 22:46:28 -06:00
vision.md feat: complete M6-M7 + Enterprise Readiness milestones; split oversized source files per CODING_GUIDELINES §9 2026-02-23 22:41:16 -07:00

Forage

A foraging engine. The Chrome extension browses, tidalDB learns, the feed sharpens.


This readme is an implementation-oriented summary. plan.md is the canonical build spec when details conflict.


What It Does

Forage is two things:

forage-engine — a library crate wrapping tidalDB with a foraging-specific signal schema, MAB exploration layer, and clean API. This is the reusable thing. Other applications embed it.

forage-server — a thin Axum HTTP server + feed page that demonstrates the engine. Click something → the next 7 items shift. Skip something → it does not come back. The preference model updates in under 100ms. The feed page handles all signal posting itself via browser fetch() — no external tooling required.

The Chrome extension's role is light: navigate to the page, snapshot the feed state before and after a session of interactions, report what changed. Three tool calls. The interesting loop runs without it.


Quickstart

# From repo root
cargo run -p forage-server --manifest-path applications/forage/server/Cargo.toml

# Feed is live at:
open http://localhost:4242

The server seeds 100 items across 8 categories on startup. Persistent by default at ~/.forage/data.

For throwaway runs:

cargo run -p forage-server --manifest-path applications/forage/server/Cargo.toml -- --ephemeral

To override the default data directory:

cargo run -p forage-server --manifest-path applications/forage/server/Cargo.toml -- --data-dir ~/.forage/data

API

Post a signal

curl -X POST http://localhost:4242/signal \
  -H "Content-Type: application/json" \
  -d '{ "user_id": 1, "item_id": 42, "signal_type": "view" }'

Signal types: view · dwell · save · skip · share

For dwell, include duration:

-d '{ "user_id": 1, "item_id": 42, "signal_type": "dwell", "duration_ms": 240000 }'

Get feed

curl "http://localhost:4242/feed?user=1&limit=7"

Each item in the response includes a label field:

  • "match" — near a confirmed interest centroid
  • "exploring" — from an underexplored category (the MAB exploration slot)
  • "trending" — high velocity, regardless of personalization
  • "resurfaced" — previously low-engagement content being re-checked

List all items

curl http://localhost:4242/items

The Exploration Budget

Every feed response includes approximately 1 exploration item (14% of 7 items, rounded up). This item comes from a category where the user has fewer than 5 signals. It is labeled "exploring".

When you engage with an exploration item, that category graduates toward the interest model. When you skip it, the MAB deprioritizes that exploration arm. Over time, exploration items land more often — the system learns your exploration tolerance.


Pre-built Users

Three users are seeded on startup:

User ID State Description
1 Cold start No signals. Pure exploration feed.
2 Light signals ~10 signals across 2-3 categories. Partial preference model.
3 Converged ~50 signals, 2 dominant interest categories. Strong personalization.

Switch between users via the user dropdown in the feed page, or ?user=N in the API.


Running a Demo Session With Claude

In P0, Claude's role is observer only — the feed page posts its own signals. Ask Claude to:

"Navigate to localhost:4242 and snapshot the feed. I'm going to interact with it for a few minutes. After I'm done, read the page again and tell me how the feed composition shifted — which categories rose, which fell, what exploration items appeared."

Claude will:

  1. navigate to localhost:4242
  2. read_page — snapshot the initial feed (one call)
  3. Stay idle while you interact with the feed normally in the browser
  4. read_page again when you say you're done — snapshot the final feed (one call)
  5. Report: category distribution before vs. after, which labels changed, what the MAB surfaced

Three MCP tool calls total. No token-burning click-by-click automation. The loop runs in the page.

This is the P0 acceptance test.


Categories

The seed corpus covers 8 categories:

Category Items Sample
tech 15 Distributed systems, CRDTs, WAL internals
music 10 Production, mixing, composition process
jazz 15 Coltrane changes, rhythm lineage, free jazz
cooking 12 Fermentation, sourdough chemistry, miso
fitness 10 Loaded carries, mobility, walking
travel 10 City guides, route essays, local craft cultures
science 15 Emergence, small worlds, power laws
literature 13 Essays, criticism, long-form craft

Architecture

Chrome Extension (Claude, observer)
        │  read_page (before/after)
        ▼
 Feed Page (browser, localhost:4242)
        │  fetch() — signals + feed polling
        ▼
 forage-server (Axum, thin)
        │  Rust function calls
        ▼
 forage-engine (library crate)
        │  MAB · schema · seed corpus
        ▼
   tidalDB (embedded, in-process)

See architecture.md for the full data flow, signal schema, preference evolution, and MAB implementation.


What This Proves

This is the proof-of-concept for tidalDB's core thesis:

A single embedded database can replace the 6-system content ranking stack — and the applications you can build on top of it could not exist any other way.

Forage's feedback loop requires a signal-to-re-rank latency under 100ms. On the 6-system stack (Redis → Kafka → feature store sync → ranking service), this is not achievable. tidalDB closes the loop in-process: signal write, preference vector update, and re-rank are the same operation.

The MAB exploration layer — finding things at the edge of your interest graph, learning your exploration tolerance — requires a live semantic preference model that updates with every interaction. This requires the vector index and the preference vector to be the same data structure, updated atomically. That is tidalDB.

Forage is not the product. Forage is the demo that makes someone say "I get it now."


Embedding the Engine in Another App

# your-app/Cargo.toml
[dependencies]
forage-engine = { path = "path/to/forage/engine" }
use forage_engine::{ForageEngine, SignalType};
use std::path::Path;

let engine = ForageEngine::persistent(Path::new("/home/you/.forage/data"))?;
engine.seed_default_corpus()?;

engine.signal(user_id, item_id, SignalType::View)?;
engine.signal_dwell(user_id, item_id, 240_000)?;

let feed = engine.feed(user_id, 7)?;
for item in feed {
    println!("{} [{}]", item.title, item.label);
}

The Axum server is not required. The engine runs anywhere tidalDB runs — which is anywhere Rust runs.


See Also