diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index f44ee37..8660763 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -1812,6 +1812,9 @@ func nearestFlightQuery(query string) bool { // search-engine snippets from becoming a human-facing answer when a search // term has a common non-travel meaning. func researchFindingMatchesIntent(query string, finding epistemics.ResearchFinding) bool { + if !findingContainsRequestedIdentifier(query, finding) { + return false + } if !nearestFlightQuery(query) { return true } @@ -1841,6 +1844,24 @@ func researchFindingMatchesIntent(query string, finding epistemics.ResearchFindi return true } +// findingContainsRequestedIdentifier prevents a generic landing page from +// being presented as a result for a concrete model, article or brand request. +// Identifiers are detected by their syntax, not by a catalogue of providers or +// entities, so the rule stays valid as user vocabulary changes. +func findingContainsRequestedIdentifier(query string, finding epistemics.ResearchFinding) bool { + identifiers := regexp.MustCompile(`(?i)[a-z0-9][a-z0-9._-]{3,}`).FindAllString(query, -1) + if len(identifiers) == 0 { + return true + } + evidence := strings.ToLower(strings.Join([]string{finding.Source.Title, finding.Source.Locator, finding.Excerpt.Text}, " ")) + for _, identifier := range identifiers { + if !strings.Contains(evidence, strings.ToLower(identifier)) { + return false + } + } + return true +} + // nearestFlightRouteTerms retains only the two endpoint-shaped terms from a // structured flight search. It has no city or provider vocabulary: endpoint // relevance is determined from the request itself. diff --git a/cmd/core-runtime/main_test.go b/cmd/core-runtime/main_test.go index f993504..b7da5a8 100644 --- a/cmd/core-runtime/main_test.go +++ b/cmd/core-runtime/main_test.go @@ -58,6 +58,30 @@ func TestNearestFlightEvidenceRequiresBothRouteEndpointsWhenKnown(t *testing.T) } } +func TestResearchReplyRejectsGenericLandingPageForConcreteIdentifier(t *testing.T) { + result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{ + State: epistemics.FindingSupported, + Source: epistemics.Source{Locator: "https://market.example", Title: "Интернет-магазин"}, + Excerpt: epistemics.SourceExcerpt{Text: "Широкий ассортимент товаров и услуг."}, + }}} + reply, backed := renderResearchReply("на WB бренд marka-wear", result, nil) + if backed || !strings.Contains(reply, "Проверяемых данных") { + t.Fatalf("generic page was presented as a brand result: %q", reply) + } +} + +func TestResearchReplyKeepsFindingContainingConcreteIdentifier(t *testing.T) { + result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{ + State: epistemics.FindingSupported, + Source: epistemics.Source{Locator: "https://market.example/marka-wear", Title: "Marka-wear"}, + Excerpt: epistemics.SourceExcerpt{Text: "Бренд marka-wear: товары и карточки."}, + }}} + reply, backed := renderResearchReply("на WB бренд marka-wear", result, nil) + if !backed || !strings.Contains(strings.ToLower(reply), "marka-wear") { + t.Fatalf("concrete identifier finding was rejected: %q", reply) + } +} + func TestResponseReviewPreservesAttemptedResearchOutcome(t *testing.T) { if reviewMayReplacePlanningReply(false, true, false, "Уточните аэропорт.", "Пока не смог получить проверяемый ответ.") { t.Fatal("review replaced an honest unbacked research outcome")