feat(fb-35): verbatim fetch (chunk / doc / span) #126

Merged
altair823 merged 14 commits from feat/fb-35-verbatim-fetch into main 2026-05-09 16:10:42 +00:00
3 changed files with 20 additions and 9 deletions
Showing only changes of commit b86b763dfb - Show all commits

View File

@@ -310,10 +310,20 @@ fn fetch_chunk_context_at_first_chunk_clamps_lower_bound() {
},
)
.unwrap();
// context_before may be empty if target is the first chunk;
// context_after should have ≤ 2 entries. Both clamped at doc boundaries.
// p9-fb-35 R2: doc has 3 chunks; ±2 should clamp the total
// neighbor count to ≤ 2 + 1 (= excludes target).
//
// ⚠ Strict "first-chunk → context_before is empty" cannot be
// asserted here yet because chunks.ordinal column does not exist
// — `list_chunk_ids_for_doc` orders by `(created_at, chunk_id)`

회차 2 — nit. 테스트 이름은 at_first_chunk_clamps_lower_bound 인데 실제 단언은 before + after <= 4 (상한만 검사). 첫 청크가 target 일 때 context_before.is_empty()context_after.len() <= 2 를 따로 잡아주면 "clamp lower bound" 의도가 명확해짐. 또 "First" 검색 hit 가 항상 첫 청크라는 보장도 없으므로 chunks[0].chunk_id 같은 식으로 명시적으로 첫 청크를 잡으면 더 robust. 현재 테스트는 "<= 4 만 통과하면 OK" 라 회귀 잡기엔 약함.

회차 2 — nit. 테스트 이름은 `at_first_chunk_clamps_lower_bound` 인데 실제 단언은 `before + after <= 4` (상한만 검사). 첫 청크가 target 일 때 `context_before.is_empty()` 와 `context_after.len() <= 2` 를 따로 잡아주면 "clamp lower bound" 의도가 명확해짐. 또 `"First"` 검색 hit 가 항상 첫 청크라는 보장도 없으므로 `chunks[0].chunk_id` 같은 식으로 명시적으로 첫 청크를 잡으면 더 robust. 현재 테스트는 "`<= 4` 만 통과하면 OK" 라 회귀 잡기엔 약함.
// and chunk_id is a blake3 hash, so the "First chunk" content
// may land at any hash-order position within the doc. The clamp
// logic itself is correct (target_idx ± n → [0..len]); we just
// can't pin which chunk is hash-order-first. Tracked as
// follow-up: V007 chunks.ordinal migration.
let total = result.context_before.len() + result.context_after.len();
assert!(
result.context_before.len() + result.context_after.len() <= 4,
"doc boundary should clamp ±N to fit chunk count"
total <= 2,

타이트닝 적절. 3-chunk doc + N=2 → 이웃 ≤ 2 는 산술적으로 정확하고 (target 제외), 코멘트가 "왜 strict first-chunk-empty 못 박는지" 를 hash-order vs ordinal 차이로 정직하게 설명. 과도하지 않고 V007 ordinal 마이그레이션 이후 강화 여지 남김.

타이트닝 적절. 3-chunk doc + N=2 → 이웃 ≤ 2 는 산술적으로 정확하고 (target 제외), 코멘트가 "왜 strict first-chunk-empty 못 박는지" 를 hash-order vs ordinal 차이로 정직하게 설명. 과도하지 않고 V007 ordinal 마이그레이션 이후 강화 여지 남김.
"doc with 3 chunks ±2 → at most 2 neighbors (excludes target), got {total}"
);
}

View File

@@ -389,10 +389,11 @@ impl SqliteStore {
///

회차 2 — nit. 위에서는 "Within one ingest transaction all chunks share created_at to the millisecond" 라 hash sort 가 dominant 라고 명시했는데, 아래 current behavior is good enough for sequentially chunked markdown where created_at uniqueness varies 부분이 약간 모순적으로 읽힌다 (created_at 이 같은 ms 안에서는 markdown 도 PDF 도 동일하게 hash sort 가 지배). 메시지를 "good enough for short docs where chunk_id hash collisions with doc-position happen to align — large markdown / PDF can re-order" 정도로 다듬으면 의도가 더 분명해짐.

회차 2 — nit. 위에서는 "Within one ingest transaction all chunks share `created_at` to the millisecond" 라 hash sort 가 dominant 라고 명시했는데, 아래 `current behavior is good enough for sequentially chunked markdown where created_at uniqueness varies` 부분이 약간 모순적으로 읽힌다 (`created_at` 이 같은 ms 안에서는 markdown 도 PDF 도 동일하게 hash sort 가 지배). 메시지를 `"good enough for short docs where chunk_id hash collisions with doc-position happen to align — large markdown / PDF can re-order"` 정도로 다듬으면 의도가 더 분명해짐.
/// Real fix is a `chunks.ordinal` column (V007 migration) or sort
/// by `chunks.source_spans_json[0]` start offset. Tracked as
/// follow-up; current behavior is good enough for sequentially
/// chunked markdown where created_at uniqueness varies, but PDFs
/// (page-aligned chunks) and large docs may surprise the agent.
/// See `tasks/HOTFIXES.md` if/when this is escalated.
/// follow-up. Until then `--context` neighbors are best-effort —
/// they may or may not align with document position depending on
/// whether `chunk_id` hash order happens to match insertion order
/// for that particular doc. Large markdown / PDF (page-aligned
/// chunks) likely re-orders. See `tasks/HOTFIXES.md` if escalated.

모순 제거 깔끔. "hash sort dominates" 문단과 일치하게 "best-effort, may or may not align" 으로 정리됨 — 위 문단이 "chunk_id sort 가 지배한다" 고 단정해놓고 아래에서 "sequentially chunked markdown 에서는 괜찮다" 고 했던 R2 지적사항이 해소.

모순 제거 깔끔. "hash sort dominates" 문단과 일치하게 "best-effort, may or may not align" 으로 정리됨 — 위 문단이 "chunk_id sort 가 지배한다" 고 단정해놓고 아래에서 "sequentially chunked markdown 에서는 괜찮다" 고 했던 R2 지적사항이 해소.
pub fn list_chunk_ids_for_doc(
&self,
doc_id: &kebab_core::DocumentId,

View File

@@ -18,7 +18,7 @@
"text": { "type": "string", "description": "kind=doc/span: markdown text (truncated if budget tripped)" },
"line_start": { "type": ["integer", "null"], "minimum": 1, "description": "kind=span: requested start line (1-based)" },
"line_end": { "type": ["integer", "null"], "minimum": 1, "description": "kind=span: requested end line (1-based, inclusive)" },
"effective_end": { "type": ["integer", "null"], "minimum": 1, "description": "kind=span: actual emitted end line after budget truncation" },
"effective_end": { "type": ["integer", "null"], "minimum": 0, "description": "kind=span: actual end line of emitted text (1-based, inclusive). Equals `line_end` on full slice; less than `line_end` when (a) requested range exceeded total lines (line clamp) or (b) `--max-tokens` budget trimmed the tail. Special case: `line_start - 1` (which is 0 when line_start=1) signals the entire requested range was beyond doc end — returned `text` is empty." },

회차 2 — round-1 panic-fix 부수효과로 wire schema 가 깨졌다. 새 early-return 은 line_start = 1 일 때 effective_end = Some(0) 을 직렬화하는데, schema 의 "minimum": 1 이 이를 거절. 또 description 도 "after budget truncation" 만 언급해서 line-clamp / out-of-range sentinel (effective_end < line_start) 의미가 빠짐. 둘 중 하나로 정리: (a) "minimum": 0 + description 갱신 (line-clamp + out-of-range sentinel 의미 추가), 또는 (b) out-of-range 시 effective_end: None 직렬화 (이미 nullable). (b) 가 schema 표면 유지 + 의미도 자연스러움 ("no lines fetched" → 값 없음). MCP / 외부 통합이 schema 를 strict validate 하면 무조건 reject 되니 머지 전에 정리 권장.

회차 2 — round-1 panic-fix 부수효과로 wire schema 가 깨졌다. 새 early-return 은 `line_start = 1` 일 때 `effective_end = Some(0)` 을 직렬화하는데, schema 의 `"minimum": 1` 이 이를 거절. 또 description 도 `"after budget truncation"` 만 언급해서 line-clamp / out-of-range sentinel (effective_end < line_start) 의미가 빠짐. 둘 중 하나로 정리: (a) `"minimum": 0` + description 갱신 (line-clamp + out-of-range sentinel 의미 추가), 또는 (b) out-of-range 시 `effective_end: None` 직렬화 (이미 nullable). (b) 가 schema 표면 유지 + 의미도 자연스러움 ("no lines fetched" → 값 없음). MCP / 외부 통합이 schema 를 strict validate 하면 무조건 reject 되니 머지 전에 정리 권장.

스키마-구현 일치 깔끔. minimum: 0Some(line_start.saturating_sub(1)) (fetch.rs:258, line_start=1 out-of-range case) 을 정확히 수용하고, description 이 (a) line clamp / (b) budget trim / (c) empty-text sentinel 세 케이스를 모두 명시해서 외부 통합자가 분기 규칙을 코드 보지 않고도 알 수 있음.

스키마-구현 일치 깔끔. `minimum: 0` 이 `Some(line_start.saturating_sub(1))` (fetch.rs:258, line_start=1 out-of-range case) 을 정확히 수용하고, description 이 (a) line clamp / (b) budget trim / (c) empty-text sentinel 세 케이스를 모두 명시해서 외부 통합자가 분기 규칙을 코드 보지 않고도 알 수 있음.
"truncated": { "type": "boolean", "description": "kind=doc/span: budget forced text truncation. Always false for chunk." }
}
}