Embedding dedup and the 0.80 threshold
The graph extraction engine was producing duplicate nodes. "Can't initiate phone calls" and "Phone call avoidance" are the same friction, but string matching can't see that.
Solution: 768-dimensional embedding vectors via gemini-embedding-001. Every new node gets embedded. Every existing node has a cached embedding. Cosine similarity determines duplicates.
The threshold design has three zones:
- ≥ 0.85: Automatic merge. The nodes are semantically identical.
- 0.80–0.85: Fuzzy zone. An additional LLM call arbitrates — are these genuinely the same friction, or subtly different?
- < 0.80: New node. Distinct enough to stand on its own.
The EmbeddingCache class (71 lines) stores embeddings in memory with findBestMatch() returning the best candidate above 0.80. The cosineSimilarity() function is pure math — no API calls, runs client-side.
The user specifically approved using gemini-embedding-001 over the legacy text-embedding-004, noting: "Lever your Gemini documentation skills and plugins to verify you're doing this well with the latest and greatest." The newer model supports MRL dimensionality scaling — 768 dims matches the original spec while getting better quality vectors.
Test script at scripts/test-dedup.ts (183 lines) verifies cosine similarity math, identical/orthogonal/similar vector cases, and merge operations.