fix(embeddings): no chunk is a heading alone (#4784)

Judging live auto-inject menus (#4772) found "## Work log — 2026-08-21",
"## Findings worth carrying forward" and "Two dev→main PRs this session."
each embedded as a whole chunk. Text about nothing embeds close to
everything, so they took ranks 1–3 on vague queries and pushed real records
down.

Three ways the chunker made them, each closed:
- _split_paragraphs flushed a heading on its own when the paragraph under it
  was long. A thin lead is now carried into the paragraph that follows, and
  a hard cut never lands in the first quarter of the budget, where the
  newline it finds closes a heading.
- A heading with no body (above its ### parts) became a section of its own
  when the section before it was full. A thin section now takes the next.
- A short lead-in or a short last section stood alone. The lead-in joins
  what follows; a thin tail joins what precedes it.

A continuation piece now repeats the LAST heading before it, since one
section can now hold several. CHUNKER_VERSION 2 → 3, so the startup backfill
re-embeds; tuned dials will report their shape as changed, which is true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 21:25:37 -04:00
co-authored by Claude Opus 5.5
parent 5d0e97576b
commit 78fa01b025
2 changed files with 167 additions and 15 deletions
+84
View File
@@ -8,8 +8,11 @@ untouched), and long records lose NOTHING — every line of the body lands in
some chunk, each chunk inside the window budget, each carrying the title as
its topical anchor.
"""
import re
from scribe.services.embeddings import (
_CHUNK_CHAR_BUDGET,
_MIN_CHUNK_CONTENT,
chunk_document,
embedding_text,
)
@@ -136,6 +139,87 @@ def test_a_monster_single_paragraph_is_hard_split_not_dropped():
assert total_words == 2000
# --- no chunk is a heading alone (#4784) -------------------------------------
#
# Found by judging live auto-inject menus: "## Work log — 2026-08-21",
# "## Findings worth carrying forward" and "Two dev→main PRs this session."
# were each a whole chunk, and took ranks 1–3 on vague queries — text about
# nothing embeds close to everything. Each case below is one of those shapes.
_HEADING = re.compile(r"^#{1,6}\s")
def _said(chunk: str, title: str) -> str:
"""A chunk's text once its title prefix and heading lines are set aside."""
body = chunk[len(title) + 1:] if chunk.startswith(title + "\n") else chunk
return "\n".join(ln for ln in body.splitlines() if not _HEADING.match(ln)).strip()
def _assert_nothing_thin_and_nothing_lost(title: str, body: str) -> list[str]:
chunks = chunk_document(title, body)
assert len(chunks) > 1, "the case must be long enough to split"
for chunk in chunks:
assert len(_said(chunk, title)) >= _MIN_CHUNK_CONTENT, (
f"a chunk says almost nothing: {chunk[:120]!r}")
assert len(chunk) <= _CHUNK_CHAR_BUDGET + len(title) + 1 + 80
joined = "\n".join(chunks)
for line in (ln.strip() for ln in body.splitlines()):
# A line longer than a chunk is cut across two, so only its ends can
# be looked for whole.
for part in ([line] if len(line) < 400 else [line[:60], line[-60:]]):
assert part in joined, f"content dropped: {part[:60]!r}"
return chunks
def test_a_heading_over_one_long_paragraph_is_not_a_chunk_of_its_own():
"""A work log whose entry is one long paragraph: the split used to flush
the heading as its own piece before cutting the paragraph."""
entry = " ".join(["The deploy was verified against the live instance."] * 40)
_assert_nothing_thin_and_nothing_lost(
"Step 6 — webhook drift-flagging",
"The step body.\n\n## Work log — 2026-08-16\n\n" + entry,
)
def test_a_heading_with_nothing_under_it_joins_what_follows():
"""'## Findings' directly above its '### …' parts is a section with no
body, and stood alone whenever the section before it was full."""
body = (
# Five paragraphs fill the section before it, so the heading cannot
# merge backwards and has to be carried forward.
"## Context\n\n" + _long_section("context", paragraphs=5)
+ "\n\n## Findings worth carrying forward\n\n"
+ "### One\n\n" + _long_section("one", paragraphs=5)
+ "\n\n### Two\n\n" + _long_section("two", paragraphs=5)
)
_assert_nothing_thin_and_nothing_lost("A milestone dev-log", body)
def test_a_lone_lead_in_line_joins_the_section_after_it():
_assert_nothing_thin_and_nothing_lost(
"2026-06-10 — UI batch",
"Two dev→main PRs this session.\n\n## PR 89\n\n"
+ _long_section("pr89", paragraphs=8),
)
def test_a_thin_last_section_joins_the_one_before_it():
_assert_nothing_thin_and_nothing_lost(
"T",
"## Body\n\n" + _long_section("body", paragraphs=8) + "\n\n## Next\n\nTBD.",
)
def test_a_continuation_piece_repeats_the_heading_it_sits_under():
"""A thin heading merged forward puts two headings in one section; the
pieces of the second must carry the second, not the first."""
body = "## Parent\n\n## Child\n\n" + _long_section("child", paragraphs=40)
chunks = chunk_document("T", body)
assert len(chunks) > 2
for chunk in chunks[1:]:
assert "## Child" in chunk
# --- the read path: best chunk wins (#280 step 4) ----------------------------