diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 943f790..1d04ab2 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000284-live-generic-synthesis-evidence.json", - "task_id": "DEV-000284", + "task_file": "dev/tasks/DEV-000285-semantic-research-routing-fix.json", + "task_id": "DEV-000285", "task_revision": 1 } diff --git a/dev/tasks/DEV-000285-semantic-research-routing-fix.json b/dev/tasks/DEV-000285-semantic-research-routing-fix.json new file mode 100644 index 0000000..b1edde1 --- /dev/null +++ b/dev/tasks/DEV-000285-semantic-research-routing-fix.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000285", + "task_revision": 1, + "architecture_revision": "ADR-0018", + "base_repo_revision": "5c98bc3", + "risk_tier": "E", + "intent": "Route semantically source-dependent current-fact questions to the existing provider-neutral WEB_RESEARCH capability instead of returning an unverified fallback.", + "requirement_ids": ["truth-first source routing", "no entity-specific keyword branches", "local deterministic fallback remains honest"], + "allowed_change_scope": {"modules": ["interaction"], "paths": ["dev/ACTIVE_TASK.json", "dev/tasks/DEV-000285-semantic-research-routing-fix.json", "modules/interaction/domain/chat.go", "modules/interaction/domain/chat_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": ["interaction semantic candidate contract", "capability WEB_RESEARCH hint"], "may_change": []}, + "semantic_owner": "interaction", + "canonical_state": ["no new canonical state; only semantic interpretation hints change"], + "state_transitions": ["source-dependent question -> WEB_RESEARCH hint -> existing research runtime"], + "side_effects_and_privileges": ["existing read-only research connector may be selected by downstream runtime"], + "persistence_and_migrations": ["none"], + "failure_retry_restart": ["if research is unavailable, existing truthful unverified response remains"], + "assumptions": ["lexical cues represent semantic classes such as currentness and observed external state, not named entities"], + "acceptance_scenarios": ["weather-like current fact routes to WEB_RESEARCH without city-specific code", "ordinary model mention does not route to research", "non-research factual question remains honest when no source-dependent cue exists"], + "required_tests": ["go test ./modules/interaction/domain", "go vet ./modules/interaction/domain", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production health smoke"], + "terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY"] +} diff --git a/modules/interaction/domain/chat.go b/modules/interaction/domain/chat.go index a126dad..7989f64 100644 --- a/modules/interaction/domain/chat.go +++ b/modules/interaction/domain/chat.go @@ -405,6 +405,9 @@ func capabilityHintsFor(primary, secondary interaction.SpeechAct, tokens []strin if primary == interaction.SpeechActQuestion || primary == interaction.SpeechActRequestToInvestigate || secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActRequestToInvestigate { add("COGNITIVE_REASONING") } + if sourceDependentQuestion(primary, secondary, tokens) { + add("WEB_RESEARCH") + } if containsToken(tokens, "напомни") || containsToken(tokens, "напомин") { add("SCHEDULE_REMINDER") } @@ -446,6 +449,20 @@ func capabilityHintsFor(primary, secondary interaction.SpeechAct, tokens []strin return values } +// sourceDependentQuestion identifies semantic classes whose answer depends on +// a current or externally observed source. It intentionally uses no named +// place, person or one-off question branch; when the cue is absent the +// truth-first fallback remains in force. +func sourceDependentQuestion(primary, secondary interaction.SpeechAct, tokens []string) bool { + inquiry := primary == interaction.SpeechActQuestion || primary == interaction.SpeechActRequestToInvestigate || secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActRequestToInvestigate + if !inquiry { + return false + } + currentCue := containsStemInTokens(tokens, "сейчас", "сегодня", "текущ", "актуаль", "последн", "прямо") + observedStateCue := containsStemInTokens(tokens, "погод", "температур", "осад", "ветер", "курс", "цен", "стоим", "новост", "распис", "событ", "статус", "доступен", "работает", "запущен", "срок") + return currentCue || observedStateCue +} + func informationRequestKind(message string, candidate interaction.SemanticCandidate) interaction.InformationRequestKind { tokens := semanticTokens(message) if len(tokens) == 0 { diff --git a/modules/interaction/domain/chat_test.go b/modules/interaction/domain/chat_test.go index b20efe9..d6c9660 100644 --- a/modules/interaction/domain/chat_test.go +++ b/modules/interaction/domain/chat_test.go @@ -133,6 +133,25 @@ func TestInformationRequestsAreClassifiedOnceByInteraction(t *testing.T) { } } +func TestSourceDependentQuestionRequestsWebResearchWithoutEntityBranch(t *testing.T) { + candidate := BuildDeterministicCandidate("какая погода в Киеве?", "", time.Unix(10, 0).UnixMilli()) + seen := false + for _, hint := range candidate.CapabilityHints { + if hint == "WEB_RESEARCH" { + seen = true + } + } + if !seen { + t.Fatalf("source-dependent question was not routed to WEB_RESEARCH: %+v", candidate) + } + ordinary := BuildDeterministicCandidate("что такое гравитация?", "", time.Unix(10, 0).UnixMilli()) + for _, hint := range ordinary.CapabilityHints { + if hint == "WEB_RESEARCH" { + t.Fatalf("ordinary conceptual question was incorrectly routed to WEB_RESEARCH: %+v", ordinary) + } + } +} + func TestDeterministicCandidateKeepsFirstPersonCompletedWorkAsAssertion(t *testing.T) { candidate := BuildDeterministicCandidate("Я всё уже настроил.", "", time.Unix(10, 0).UnixMilli()) if candidate.PrimarySpeechAct != interaction.SpeechActAssertion {