From 069f31e1163eb573028c736a7f524d2dc516ab49 Mon Sep 17 00:00:00 2001 From: hallelx2 Date: Fri, 16 Oct 2026 04:02:50 +0100 Subject: [PATCH 1/2] fix(server): /v1/query leads with judgewalk's evidence pages; max_sections keeps them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The self-host and production server (cmd/server, internal/handler) read only SelectedIDs from a page-based strategy, so /v1/query returned the overlapping sections in document order and dropped the evidence pages HAL-1390 added to the standalone engine. With max_sections 5 the gold evidence was often cut: on the rig the 3M FY2018 capex question's section came 7th of 35. The evidence pages now lead (page_, '
· p.', page text), the same contract as internal/api, and max_sections caps the combined list so a small cap keeps the evidence. --- internal/handler/query.go | 47 ++++++++++++++++-- internal/handler/query_evidence_test.go | 63 +++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 internal/handler/query_evidence_test.go diff --git a/internal/handler/query.go b/internal/handler/query.go index 3ac34ff..1c30705 100644 --- a/internal/handler/query.go +++ b/internal/handler/query.go @@ -1,6 +1,7 @@ package handler import ( + "fmt" "context" "encoding/json" "errors" @@ -157,8 +158,9 @@ func (h *QueryHandler) HandleQuery(w http.ResponseWriter, r *http.Request) { // Use CostStrategy if available to get token usage + cost. var ( - ids []tree.SectionID - usage *retrieval.Usage + ids []tree.SectionID + usage *retrieval.Usage + evidence []retrieval.EvidencePage ) if cs, ok := strategy.(retrieval.CostStrategy); ok { result, err := cs.SelectWithCost(r.Context(), t, body.Query, budget) @@ -172,6 +174,7 @@ func (h *QueryHandler) HandleQuery(w http.ResponseWriter, r *http.Request) { } ids = result.SelectedIDs usage = &result.Usage + evidence = result.EvidencePages } else { var err error ids, err = strategy.Select(r.Context(), t, body.Query, budget) @@ -185,11 +188,21 @@ func (h *QueryHandler) HandleQuery(w http.ResponseWriter, r *http.Request) { } } - if body.MaxSections > 0 && len(ids) > body.MaxSections { - ids = ids[:body.MaxSections] + // A page-based strategy's evidence pages lead the list, as found: the + // page the Judge chose holds the answer, while the sections overlapping + // it come back in document order (HAL-1390). max_sections caps the + // combined list, so a small cap keeps the evidence, not the first + // sections of the document (HAL-3071). + sections := evidencePageSections(evidence) + if body.MaxSections > 0 { + if len(sections) > body.MaxSections { + sections = sections[:body.MaxSections] + } + if room := body.MaxSections - len(sections); len(ids) > room { + ids = ids[:room] + } } - sections := make([]map[string]any, 0, len(ids)) for _, id := range ids { sec := t.FindByID(id) if sec == nil { @@ -235,3 +248,27 @@ func (h *QueryHandler) HandleQuery(w http.ResponseWriter, r *http.Request) { setTokenHeaders(w.Header(), resp) writeJSON(w, http.StatusOK, resp) } + +// evidencePageSections turns a page-based strategy's evidence pages into +// response units: id "page_", the owning section's title with the page +// ("
· p."), the page text as content. The same shape the +// standalone engine returns (internal/api), so clients and the benchmark +// read one contract from either binary. +func evidencePageSections(pages []retrieval.EvidencePage) []map[string]any { + out := make([]map[string]any, 0, len(pages)) + for _, ep := range pages { + title := ep.Title + if title == "" { + title = fmt.Sprintf("Page %d", ep.Page) + } + out = append(out, map[string]any{ + "id": fmt.Sprintf("page_%d", ep.Page), + "title": fmt.Sprintf("%s · p.%d", title, ep.Page), + "content": ep.Text, + "page": ep.Page, + "confidence": ep.Confidence, + "token_count": len(ep.Text) / 4, + }) + } + return out +} diff --git a/internal/handler/query_evidence_test.go b/internal/handler/query_evidence_test.go new file mode 100644 index 0000000..3af5619 --- /dev/null +++ b/internal/handler/query_evidence_test.go @@ -0,0 +1,63 @@ +package handler + +import ( + "context" + "testing" + + "github.com/hallelx2/vectorless-engine/pkg/retrieval" + "github.com/hallelx2/vectorless-engine/pkg/tree" +) + +// evidenceStrategy is a page-based strategy stub: it picks both sections +// in document order and names page 4 (in section B) as the evidence. +type evidenceStrategy struct{} + +func (evidenceStrategy) Name() string { return "judgewalk" } + +func (evidenceStrategy) Select(ctx context.Context, t *tree.Tree, q string, b retrieval.ContextBudget) ([]tree.SectionID, error) { + return []tree.SectionID{"sec_a", "sec_b"}, nil +} + +func (evidenceStrategy) SelectWithCost(ctx context.Context, t *tree.Tree, q string, b retrieval.ContextBudget) (*retrieval.Result, error) { + return &retrieval.Result{ + SelectedIDs: []tree.SectionID{"sec_a", "sec_b"}, + EvidencePages: []retrieval.EvidencePage{{Page: 4, Title: "B", Text: "capex was $1,577 million", Confidence: 0.93}}, + }, nil +} + +func TestQueryLeadsWithEvidencePages(t *testing.T) { + h := newQueryStrategyHandler(evidenceStrategy{}, nil) + rec, resp := doQuery(t, h, `{"document_id":"doc_x","query":"capex?"}`) + if rec.Code != 200 { + t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) + } + units := resp["sections"].([]any) + if len(units) != 3 { + t.Fatalf("want the evidence page plus both sections, got %d units", len(units)) + } + first := units[0].(map[string]any) + if first["id"] != "page_4" || first["title"] != "B · p.4" || first["page"] != float64(4) { + t.Errorf("first unit = %v, want the evidence page", first) + } + if first["content"] != "capex was $1,577 million" { + t.Errorf("evidence page content = %v", first["content"]) + } +} + +// The bug HAL-3071 fixes: a small max_sections must keep the evidence, not +// the first sections of the document. +func TestMaxSectionsKeepsTheEvidence(t *testing.T) { + h := newQueryStrategyHandler(evidenceStrategy{}, nil) + _, resp := doQuery(t, h, `{"document_id":"doc_x","query":"capex?","max_sections":2}`) + units := resp["sections"].([]any) + if len(units) != 2 { + t.Fatalf("max_sections 2 returned %d units", len(units)) + } + if units[0].(map[string]any)["id"] != "page_4" || units[1].(map[string]any)["id"] != "sec_a" { + t.Errorf("units = %v, want page_4 then sec_a", units) + } + _, one := doQuery(t, h, `{"document_id":"doc_x","query":"capex?","max_sections":1}`) + if u := one["sections"].([]any); len(u) != 1 || u[0].(map[string]any)["id"] != "page_4" { + t.Errorf("max_sections 1 = %v, want only the evidence page", u) + } +} From b41130336218ff6483a77fcae8b89952cfc5830a Mon Sep 17 00:00:00 2001 From: hallelx2 Date: Fri, 16 Oct 2026 04:21:41 +0100 Subject: [PATCH 2/2] style(server): gofmt query.go --- internal/handler/query.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/handler/query.go b/internal/handler/query.go index 1c30705..53d3763 100644 --- a/internal/handler/query.go +++ b/internal/handler/query.go @@ -1,10 +1,10 @@ package handler import ( - "fmt" "context" "encoding/json" "errors" + "fmt" "io" "log/slog" "net/http"