Skip to content

feat(search): token-boundary matching and body-text tier [roadmap:v0.10.3] - #68

Merged
tcballard merged 3 commits into
mainfrom
claude/rac-v0.10-implementation-12w6e1
Jun 12, 2026
Merged

feat(search): token-boundary matching and body-text tier [roadmap:v0.10.3]#68
tcballard merged 3 commits into
mainfrom
claude/rac-v0.10-implementation-12w6e1

Conversation

@tcballard

@tcballard tcballard commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements rac/roadmaps/v0.10.x-guide/v0.10.3-search-quality.md.

Adds:

  • Token-boundary search matching (ADR-037): matchable text and queries tokenize on non-alphanumeric boundaries and camelCase transitions; terms match tokens by equality or prefix; multi-term queries are AND. The word-boundary false-positive class is gone — lore no longer matches Explorer artifacts.
  • Body-text search tier with snippets (ADR-038): the ranking ladder extends to identifier, title, path, section heading, body; heading/body matches carry pinned additive section and snippet fields so an agent can triage matches without retrieving each candidate.
  • Revised guide-tool-surface design: the body-text open question is settled and the snippet field shape was pinned before any code landed.
  • Re-pinned goldens, a named lore-vs-Explorer regression test, and a token-boundary test battery (+13 tests, 934 total).

Roadmap / ADR Trace

Roadmap:

  • rac/roadmaps/v0.10.x-guide/v0.10.3-search-quality.md

Relevant ADRs:

  • rac/decisions/adr-037-token-boundary-search-matching.md — the matching semantics
  • rac/decisions/adr-038-body-text-search-tier.md — the body tier, snippets, and the permanent rejection of embeddings/fuzzy/scoring
  • rac/decisions/adr-007-json-contract-stability.md — additive response rules
  • rac/decisions/adr-031, adr-032, adr-033 — consumer boundary, statelessness, response budget

Design: rac/designs/guide-tool-surface.md (revised in this PR, first commit).

Scope

Included

  • One Core matching engine in rac.services.resolve (tokenize, five-tier _match_entry, rewritten search_index) serving rac find and search_artifacts identically
  • Body/heading text sourced from the corpus snapshot via a new Product.search_sections structure that preserves original heading and line text (the existing sections map is lossy — casefolded/joined — and untouched, since other consumers depend on it); no file re-reads, exactly one corpus walk per search_artifacts call (regression-pinned)
  • rac find human output shows snippets indented under heading/body-matched rows
  • Index/repository entry models grow search_sections internally; the index JSON contract (to_dict) is unchanged

Excluded

  • Semantic, embedding, or RAG-based retrieval; stemming; synonyms; fuzzy matching; opaque scoring (ADR-038, permanent for Core)
  • Search configuration flags, pagination, or per-call ranking options
  • Tool description changes (pinned verbatim; unchanged)
  • The measurement re-run and demo recording (human-only; the roadmap gates the recording on the re-run)

Product / Architecture Decisions

  • This is a deliberate, announced behavior change to search semantics, not a bug fix: results differ from v0.10.0 wherever substring matching produced word-boundary hits. Goldens were re-pinned in the same change set and each diff reviewed as a product change (e.g. a fixture roadmap now matches markdown via its body line "Adopt Markdown everywhere", with snippet — intended body-tier behavior).
  • Empty and punctuation-only queries now match nothing (they tokenize to zero terms). Previously the empty substring matched everything. One Explorer adapter test relied on a lone dot as an accidental match-all (every path contains .md); it was updated to use a real token — the wildcard was never a documented feature.
  • Snippet fields appear only on heading/body matches and are absent (not null) on identifier/title/path matches, which remain byte-identical to v1 — the additive rule of ADR-007 applied strictly.
  • Snippet determinism: first matching line in document order, whole stored lines only; snippets ride inside match entries so the existing whole-item truncation covers them without new budget logic.
  • Resolution (rac resolve, get_artifact, get_related) is untouched: alias matching stays exact; only search matching changed.

User-Facing Contract

CLI

rac find interface unchanged. Human output gains an indented ↳ Section: line row under heading/body matches.

JSON Output

Search match entries (CLI --json and search_artifacts, identical):

{"id": "...", "type": "...", "title": "...", "path": "...", "section": "...", "snippet": "..."}

section/snippet present only on heading/body matches. All other shapes unchanged.

Exit Codes

Unchanged.

Verification

Ran

python -m pytest -q                                   # 934 passed (+13)
python -m ruff check src/ tests/                      # clean
python -m ruff format --check src/ tests/             # clean
python -m mypy src/                                   # no issues, 64 files
rac validate rac/                                     # exit 0
rac relationships rac/ --validate                     # exit 0
rac review rac/                                       # no priority 1-2 findings
rac find lore rac/                                    # only Lore artifacts, zero Explorer
rac find "delete user" examples/guide                 # demo decision still top match

Plus a live stdio MCP client: search_artifacts("lore") returned exactly the five Lore-related artifacts (title matches without snippets, body matches with), byte-equal to rac find lore --json.

Covered

  • Named regression: lore excludes Explorer artifacts on the dogfood corpus
  • Tokenization boundaries: prefix matching, camelCase splits, multi-term AND, casefolding
  • Tier ordering across all five tiers; body-only matches found with snippets; snippet truncation under small budgets
  • CLI/Guide byte-equivalence for identical state and query
  • Dogfood demo searchability tests pass UNCHANGED (delete user, delete, soft-delete still surface the demo decision — soft-delete tokenizes to [soft, delete] under AND)
  • One corpus walk per search_artifacts call; get_related's one-walk regression still passes

Review Path

  1. rac/designs/guide-tool-surface.md — the revised search contract (read first; the code implements this)
  2. src/rac/services/resolve.py — tokenizer, tiers, snippet capture
  3. src/rac/core/models.py, core/markdown.py, services/index.py, services/repository.pysearch_sections plumbing
  4. src/rac/output/human.py — snippet display
  5. tests/test_resolve.py, test_dogfood.py, test_mcp_tools.py, tests/golden/ — the re-pinned contracts

Notes For Reviewer

  • The golden diffs are the semantics change made visible — review them as product behavior, not noise.
  • Release notes for v0.10.3 should announce: token-boundary matching (results differ from substring), body-text matching with snippets, and empty/punctuation-only queries returning no matches.
  • Per the roadmap, the 10-run grounding measurement re-runs after this merges, and the demo recording happens after that gate passes.

Implementation Process

Implemented with AI assistance under the roadmap contract. Final scope, review, and acceptance decisions were made by the maintainer.

Settle the body-text open question in guide-tool-surface (ADR-037,
ADR-038): describe token-boundary matching and the five-tier ladder, and
pin the additive snippet field names ('section', 'snippet') and their
placement on heading/body match entries before any code lands.

Implements rac/roadmaps/v0.10.x-guide/v0.10.3-search-quality.md.
…p:v0.10.3]

Replace substring search with deterministic, tiered token-boundary
matching across one Core implementation that serves rac find and
search_artifacts identically.

Matching (ADR-037): identifiers, title, path, section headings, and body
text tokenize on non-alphanumeric boundaries and camelCase transitions;
a query term matches a token by casefolded equality or prefix; multi-term
queries require every term to match somewhere (AND); 'lore' no longer
matches Explorer, 'relation' still finds relationships.

Body-text tier (ADR-038): the ladder extends to id, title, path, heading,
body. Heading and body text reach search through the corpus snapshot the
walk already produces — Product gains search_sections (original heading
and line text), populated by the parser and carried on the index entry and
repository model, so no file is re-read and search_artifacts keeps one
corpus walk per call. Heading/body matches carry additive snippet fields
('section', 'snippet') with the first matching line in document order;
metadata-match entry shapes are byte-identical to before (ADR-007). The
rac find human view shows the snippet indented under matched rows.

Implements rac/roadmaps/v0.10.x-guide/v0.10.3-search-quality.md.
Add the token-boundary battery to the resolve battery: tokenization
(camelCase, non-alnum, punctuation-only), prefix matching, word-boundary
exclusion, multi-term AND, the five-tier ordering (id > title > path >
heading > body), body-only match with snippet, first-line determinism, and
the metadata-match byte-identical shape. Add the lore-vs-Explorer named
regression to the dogfood battery on the dogfood corpus.

Extend the mcp battery: a body-match snippet payload byte-equal to the CLI
find JSON, the metadata-match four-field shape, and whole-item truncation
of a snippet-bearing match.

Re-pin the find goldens to the new semantics ('markdown' now also surfaces
a roadmap by its body, with a snippet) and update the Explorer adapter's
trailing-type-token test, whose punctuation-only query no longer means
'match everything' under token matching.
@tcballard
tcballard merged commit ecf401f into main Jun 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant