(plan) system review and break down plans

This commit is contained in:
Story Crater Bot
2026-08-19 09:52:07 -07:00
commit 9d28b63ff2
46 changed files with 5620 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
# M2.4 — pgvector repository
| Field | Value |
|---|---|
| Phase | M2 — Projections |
| Size | M — 13 days |
| Status | ⬜ Not started |
| Flags | — |
| Spec | inlined below |
| Blocks | M2.3, M2.1 |
## Goal
Write the projection into Postgres idempotently, so rebuild is safe to run at any
time and produces the same rows.
## Facts (inlined — no spec read needed)
```rust
async fn upsert_node(&self, node: &MemoryNode, embedding: &[f32]) -> Result<()>;
async fn insert_edges(&self, child: &Sha256Hash, parents: &[Sha256Hash]) -> Result<()>;
async fn search(&self, q: &[f32], levels: &[Level], project: &ProjectId, k: usize)
-> Result<Vec<ScoredNode>>;
async fn parents_of(&self, sha: &Sha256Hash) -> Result<Vec<MemoryNode>>;
async fn clear_project(&self, project: &ProjectId) -> Result<()>;
```
`upsert_node` is `ON CONFLICT (sha256) DO NOTHING`. Content identity means an
identical node is the same node; re-running rebuild must not duplicate or churn
rows. Same for edges on the composite key.
`search` orders by `embedding <=> $1` — cosine distance, matching the
`vector_cosine_ops` index. Any other operator silently drops to a seq scan.
Edges are inserted **after** both endpoints exist, or the foreign key rejects
them. Rebuild therefore has two passes: all nodes, then all edges. This is not an
optimisation; a single-pass insert fails on the first forward reference.
Embeddings are generated in batches of ≤32 (M2.1) and are the expensive part of
rebuild — batch across nodes, not per node.
## Steps
1. `PgRepo::connect(url)` with a pool; run migrations on connect.
2. Implement the five methods above.
3. `upsert_many(nodes)` batching embedding calls at 32 and inserting with a
multi-row statement.
4. Two-pass write: nodes, then edges.
5. `clear_project` deletes nodes for one project; edges cascade.
6. Return `ScoredNode { node, distance }` — keep the raw distance, do not convert
to a similarity score here. The reranker (M3.2) wants the ordering, and a
lossy conversion hides ties.
## Acceptance
- Upserting the same node twice leaves one row.
- Edges referencing not-yet-inserted parents fail; two-pass write succeeds.
- `search` returns nearest-first and respects the level filter.
- `clear_project` removes only that project.
## Verify
**Harness:** disposable Postgres with the production image; a deterministic fake
embedder (hash → fixed vector) so vector assertions are exact.
**Integration test**`tests/it_pg_repo.rs`:
1. `a1_upsert_idempotent` — upsert twice, assert `count(*) == 1`.
2. `a2_two_pass_required` — single-pass insert with a forward edge reference
fails; two-pass succeeds. Proves the ordering constraint is real.
3. `a3_search_orders_by_distance` — insert three known vectors, assert returned
order matches hand-computed cosine distance.
4. `a4_level_filter` — L0/L1/L2 present; search with `levels=[L1]` returns only
L1.
5. `a5_project_isolation` — two projects with identical text; search one, assert
no cross-project results.
6. `a6_clear_project_scoped` — clear one, assert the other is intact and no
orphan edges remain.
7. `a7_batching` — upsert 100 nodes, assert the embedder saw exactly 4 calls.
8. `a8_parents_of` — walk a two-level graph, assert the returned parents match.
**Command:** `cargo test -p mem-store pg_repo`
**False pass:**
- Using a random embedder. Assertion 3 becomes untestable and is usually deleted,
which removes the only check that the distance operator matches the index.
- Testing `search` with one project in the database. Assertion 5 is the only one
that catches a missing `WHERE project = $1`, and that bug leaks another
project's memory into every answer.
## Traps
- Converting distance to similarity in the repo. It loses precision, and the
reranker downstream wants candidates in order rather than scores.
- Per-node embedding calls. 412 chunks becomes 412 HTTP round trips where 13
would do, and rebuild goes from seconds to minutes.
---
Background: [DESIGN.md](../DESIGN.md) — pgvector, retrieval