DEV-000286 Reject irrelevant research excerpts #264

Merged
m merged 1 commits from codex/dev-000248-self-development-runtime-status into main 2026-08-24 08:51:05 +00:00
4 changed files with 73 additions and 4 deletions
+31 -1
View File
@@ -24,8 +24,14 @@ var (
bingLinkPattern = regexp.MustCompile(`(?is)<li[^>]+class=["'][^"']*b_algo[^"']*["'][^>]*>.*?<h2[^>]*><a[^>]*>(.*?)</a>`)
bingSnippetPattern = regexp.MustCompile(`(?is)<li[^>]+class=["'][^"']*b_algo[^"']*["'][^>]*>.*?<p[^>]*>(.*?)</p>`)
tagPattern = regexp.MustCompile(`(?s)<[^>]*>`)
termPattern = regexp.MustCompile(`(?i)[\p{L}\p{N}]{2,}`)
)
var searchStopTerms = map[string]struct{}{
"какая": {}, "какое": {}, "какие": {}, "какой": {}, "что": {}, "как": {}, "где": {}, "когда": {}, "почему": {}, "мне": {}, "пожалуйста": {},
"what": {}, "which": {}, "where": {}, "when": {}, "why": {}, "how": {}, "please": {}, "the": {}, "this": {},
}
// SearchConnector uses a configured HTTPS search endpoint. It does not know
// cities, weather terms, entities or model names: the complete semantic query
// comes from the interpretation layer.
@@ -91,7 +97,7 @@ func (c SearchConnector) Collect(ctx context.Context, request epistemics.WebSear
snippet = cleanHTML(string(snippets[index][1]))
}
statement := strings.TrimSpace(strings.Trim(title+" — "+snippet, " —"))
if statement == "" {
if statement == "" || !searchStatementRelatesToQuery(request.Query, statement) {
continue
}
ordinal := strconv.Itoa(index)
@@ -108,6 +114,30 @@ func (c SearchConnector) Collect(ctx context.Context, request epistemics.WebSear
return results, nil
}
// searchStatementRelatesToQuery is a bounded relevance gate, not a truth
// check. It prevents generic search pages from becoming apparent evidence
// when their content has no substantive relation to the requested query.
func searchStatementRelatesToQuery(query, statement string) bool {
terms := termPattern.FindAllString(strings.ToLower(query), -1)
content := strings.ToLower(statement)
usable := 0
for _, term := range terms {
if _, stop := searchStopTerms[term]; stop || len([]rune(term)) < 4 {
continue
}
usable++
runes := []rune(term)
prefixLen := 5
if len(runes) < prefixLen {
prefixLen = len(runes)
}
if strings.Contains(content, string(runes[:prefixLen])) {
return true
}
}
return usable == 0
}
func (c SearchConnector) allowedHost(host string) bool { _, ok := c.AllowedHosts[host]; return ok }
func (c SearchConnector) limit() int {
if c.MaxResults <= 0 || c.MaxResults > 8 {
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000285-semantic-research-routing-fix.json",
"task_id": "DEV-000285",
"task_file": "dev/tasks/DEV-000286-research-result-relevance-filter.json",
"task_id": "DEV-000286",
"task_revision": 1
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000286",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "b0a8b3e",
"risk_tier": "E",
"intent": "Reject web-search excerpts with no substantive lexical relation to the requested query so truth-first chat never presents irrelevant search results as useful evidence.",
"requirement_ids": ["research relevance boundary", "truth-first no irrelevant evidence", "no entity-specific filtering"],
"allowed_change_scope": {"modules": ["research adapter"], "paths": ["dev/ACTIVE_TASK.json", "dev/tasks/DEV-000286-research-result-relevance-filter.json", "adapters/research/search_connector.go", "tests/research_runtime/search_connector_test.go"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "cmd/**", "deploy/**", "LLM-local/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["epistemics ResearchCandidate", "provider-neutral WebSearchRequest"], "may_change": []},
"semantic_owner": "epistemics research adapter boundary",
"canonical_state": ["no new canonical state; irrelevant candidates are not emitted"],
"state_transitions": ["raw search result -> relevance gate -> candidate or omission"],
"side_effects_and_privileges": ["read-only search connector only"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["zero relevant candidates becomes the existing truthful no-evidence response"],
"assumptions": ["short query terms without substantive tokens retain prior parser behavior for compatibility", "term overlap is a bounded relevance guard, not semantic truth verification"],
"acceptance_scenarios": ["irrelevant dictionary results are omitted for a weather-like query", "relevant result is retained", "no entity-specific branch exists"],
"required_tests": ["go test ./tests/research_runtime", "go vet ./adapters/research ./tests/research_runtime", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production health smoke", "one live chat smoke"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY"]
}
@@ -16,7 +16,7 @@ func TestSearchConnectorReturnsBoundedImmutableEvidence(t *testing.T) {
_, _ = w.Write([]byte(`<a class="result__a">Verified title</a><a class="result__snippet">Observed source excerpt</a>`))
}))
defer server.Close()
request := epistemics.WebSearchRequest{Plan: epistemics.ResearchPlan{ID: "plan", Question: "current fact", SourceClasses: []string{"web-search"}, FreshnessPolicy: "current"}, PropositionKey: "question-1", Query: "current fact"}
request := epistemics.WebSearchRequest{Plan: epistemics.ResearchPlan{ID: "plan", Question: "verified source", SourceClasses: []string{"web-search"}, FreshnessPolicy: "current"}, PropositionKey: "question-1", Query: "verified source"}
connector := researchadapter.SearchConnector{EndpointTemplate: server.URL + "?q=%s", AllowedHosts: map[string]struct{}{"127.0.0.1": {}}, Client: server.Client(), MaxResults: 3}
// The production connector requires HTTPS. The test server is HTTPS too;
// use its host as the allowlisted value and preserve the same transport.
@@ -52,3 +52,19 @@ func TestSearchConnectorParsesConfiguredBingStyleResults(t *testing.T) {
t.Fatalf("unexpected Bing-style evidence: candidates=%+v err=%v", candidates, err)
}
}
func TestSearchConnectorDropsIrrelevantResults(t *testing.T) {
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`<li class="b_algo"><h2><a href="https://source.example">Викисловарь</a></h2><p>Статья о грамматике</p></li>`))
}))
defer server.Close()
request := epistemics.WebSearchRequest{Plan: epistemics.ResearchPlan{ID: "plan", Question: "какая погода в Киеве", SourceClasses: []string{"web-search"}, FreshnessPolicy: "current"}, PropositionKey: "p", Query: "какая погода в Киеве"}
connector := researchadapter.SearchConnector{EndpointTemplate: server.URL + "?q=%s", AllowedHosts: map[string]struct{}{"127.0.0.1": {}}, Client: server.Client(), MaxResults: 3}
candidates, err := connector.Collect(context.Background(), request, 10)
if err != nil {
t.Fatalf("collect search evidence: %v", err)
}
if len(candidates) != 0 {
t.Fatalf("irrelevant result was retained: %+v", candidates)
}
}