별칭을 줄별 개별 dense 벡터(sentinel `{chunk}#alias#N`)로 색인하고
boilerplate 청크는 별칭 생성을 skip. 묶음 1벡터 방식은 평균화로 특정
표현이 희석돼 오히려 회귀(13/18)했던 것을 폐기. 변형 일관성 14/18 →
16/18, mean_spread@10 0.222 → 0.111 (나무위키 ~1000 문서 CS corpus).
`kebab-core::strip_alias_suffix` 가 suffix 형과 per-alias 형 둘 다 처리.
파생물 캐시(V012): embedding 벡터 + 별칭 LLM 결과를 청크 내용 해시
키로 캐싱해 재색인 시 내용 불변 청크의 재계산을 skip. cache_key =
blake3(kind ‖ text_blake3 ‖ version_key)[:32], version_key 에
model/prompt/dimensions 포함 → §9 cascade 와 정합(버전 bump 시 자동
miss). 측정: 정답 3개 cold 1879s → warm 13s ≈ 145배. 순수 가산이라
corpus_revision bump 없음. search/ask 는 kebab.sqlite+lancedb 만으로
동작 → 외부 서버 색인 후 DB 만 복사하는 이식 워크플로 가능.
V012 schema migration + 신규 surface 로 workspace version 0.20.2 →
0.21.0 (minor) bump. README/HANDOFF/ARCHITECTURE/HOTFIXES sync.
known limitation: stack·svm 설명형 2개 잔존 + grounded 판정이 부분
인용을 grounded 로 오분류(후속 후보).
측정 상세: docs/superpowers/handoffs/2026-05-31-namu-wiki-alias-cache-study.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
62 lines
1.9 KiB
Rust
62 lines
1.9 KiB
Rust
//! Derivation-cache payload encoding helpers (design 2026-05-31 §3.3).
|
||
//!
|
||
//! - embedding: `dimensions × f32` little-endian bytes (1024×4 = 4096 B/chunk).
|
||
//! - alias / korean_tokens: UTF-8 as-is (handled inline by the caller — no
|
||
//! helper needed, `String::as_bytes` / `String::from_utf8`).
|
||
|
||
/// Encode an embedding vector as a little-endian `f32` byte string (§3.3).
|
||
pub fn encode_embedding(vector: &[f32]) -> Vec<u8> {
|
||
let mut out = Vec::with_capacity(vector.len() * 4);
|
||
for &v in vector {
|
||
out.extend_from_slice(&v.to_le_bytes());
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Decode a little-endian `f32` byte string back into a vector (§3.3).
|
||
///
|
||
/// Returns `None` if the payload length is not a multiple of 4 (corrupt
|
||
/// entry) — the caller treats this as a cache miss and recomputes, so a bad
|
||
/// payload never produces a wrong vector.
|
||
pub fn decode_embedding(payload: &[u8]) -> Option<Vec<f32>> {
|
||
if payload.len() % 4 != 0 {
|
||
return None;
|
||
}
|
||
Some(
|
||
payload
|
||
.chunks_exact(4)
|
||
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
|
||
.collect(),
|
||
)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn roundtrips_vector() {
|
||
let v = vec![0.0_f32, 1.5, -2.25, 3.125e10, f32::MIN, f32::MAX];
|
||
let bytes = encode_embedding(&v);
|
||
assert_eq!(bytes.len(), v.len() * 4);
|
||
assert_eq!(decode_embedding(&bytes), Some(v));
|
||
}
|
||
|
||
#[test]
|
||
fn empty_vector_roundtrips() {
|
||
assert_eq!(encode_embedding(&[]), Vec::<u8>::new());
|
||
assert_eq!(decode_embedding(&[]), Some(vec![]));
|
||
}
|
||
|
||
#[test]
|
||
fn misaligned_payload_is_none() {
|
||
assert_eq!(decode_embedding(&[1, 2, 3]), None);
|
||
}
|
||
|
||
#[test]
|
||
fn little_endian_layout_is_fixed() {
|
||
// 1.0_f32 == 0x3F800000, little-endian bytes [0x00,0x00,0x80,0x3F].
|
||
assert_eq!(encode_embedding(&[1.0]), vec![0x00, 0x00, 0x80, 0x3F]);
|
||
}
|
||
}
|