From 52074e289513393cd4eb686f47395562a194021d Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 18 Aug 2026 16:40:46 +0300 Subject: [PATCH 01/32] Move cognition-first interpretation for free-form Russian --- .../interpretation_live_trace_test.go | 79 ++++++- cmd/core-runtime/cognitive_interpretation.go | 9 +- cmd/core-runtime/main.go | 4 +- modules/interaction/domain/chat.go | 192 +++++++----------- modules/interaction/domain/chat_test.go | 13 +- modules/interaction/domain/semantics.go | 88 +++++++- modules/interaction/domain/semantics_test.go | 78 ++++--- modules/work/domain/planning.go | 42 +++- 8 files changed, 312 insertions(+), 193 deletions(-) diff --git a/cmd/cognitive-worker/interpretation_live_trace_test.go b/cmd/cognitive-worker/interpretation_live_trace_test.go index 8b89c76..d4fdf5a 100644 --- a/cmd/cognitive-worker/interpretation_live_trace_test.go +++ b/cmd/cognitive-worker/interpretation_live_trace_test.go @@ -17,6 +17,7 @@ import ( "git.cin.su/m/Assistant/contracts/cognitive" "git.cin.su/m/Assistant/contracts/identity" "git.cin.su/m/Assistant/contracts/interaction" + "git.cin.su/m/Assistant/platform/ids" "git.cin.su/m/Assistant/contracts/situation" interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain" ) @@ -74,7 +75,28 @@ func TestLiveInterpretationTrace(t *testing.T) { now: time.Now, allowCanary: true, } - for _, message := range []string{"ты кто", "можешь расказать об утре"} { + for _, message := range []string{ + "расскажи как это работает", + "Расскажи, как это работает?", + "можешь объяснить почему база растёт", + "почему postgres использует столько памяти", + "разберись почему сервис упал", + "посмотри что произошло", + "исправь ошибку если найдёшь", + "разберись почему падает и исправь если причина очевидна", + "нет не UPO а UPO2", + "нет не UPO а UPO2 и обнови конфиг", + "postgres сейчас работает", + "основная база называется UPO", + "привет", + "спасибо", + "ты кто", + "ты кто?", + "как тебя зовут", + "кто ты вообще", + "что ты думаешь", + "расскажи подробнее", + } { conversation := interaction.Conversation{ID: "trace-conversation", ActorID: "trace-actor", PrincipalID: "domovoy-creator-principal", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen} utterance, _, _, err := interactiondomain.NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: message}, time.Now()) if err != nil { @@ -121,15 +143,27 @@ func TestLiveInterpretationTrace(t *testing.T) { if rawResponse == "" { t.Fatalf("message=%q raw response is empty", message) } - if message == "ты кто" { - if semantic.Interpretation.SpeechAct != interaction.SpeechActQuestion || !semantic.NeedsCognition { - t.Fatalf("semantic path still fails punctuation-free identity question: %+v", semantic.Interpretation) - } - if gatewayResponse.Candidate.PrimarySpeechAct != "QUESTION" { - t.Fatalf("gateway did not preserve question semantics: %+v", gatewayResponse.Candidate) - } + cognitiveCandidate, err := cognitiveCandidateFromGateway(utterance.ID, gatewayResponse.Candidate, time.Now()) + if err != nil { + t.Fatal(err) } - t.Logf("TRACE message=%q deterministic=%s/%s needs_cognition=%t raw=%q gateway_candidate=%+v", message, semantic.Interpretation.SpeechAct, semantic.Interpretation.Applicability, semantic.NeedsCognition, truncate(rawResponse, 180), gatewayResponse.Candidate) + selected := interactiondomain.ApplyCognitiveCandidate(semantic, utterance.Content, cognitiveCandidate, nil) + cognitionUsed := selected.Interpretation.Source == string(interaction.CandidateSourceCognitive) + clarificationReason := string(selected.Interpretation.Clarification) + t.Logf("TRACE message=%q", message) + deterministic := semantic.Candidates[0] + t.Logf(" deterministic candidate: %+v", deterministic) + t.Logf(" raw response=%q", truncate(rawResponse, 180)) + t.Logf(" cognitive candidate: %+v", cognitiveCandidate) + t.Logf(" selected interpretation: act=%s secondary=%s selection_source=%s needs_cognition=%t", + selected.Interpretation.SpeechAct, + selected.Interpretation.SecondarySpeechAct, + selected.Interpretation.Source, + selected.Interpretation.NeedsCognition, + ) + t.Logf(" cognition used: %t", cognitionUsed) + t.Logf(" clarification reason: %s", clarificationReason) + t.Logf(" reply=%q", selected.Reply) } } @@ -217,3 +251,30 @@ func truncate(value string, max int) string { } return value[:max] + "..." } + +func cognitiveCandidateFromGateway(utteranceID interaction.HumanUtteranceID, candidate cognitive.GatewayInterpretationCandidate, now interface{ UnixMilli() int64 }) (interaction.SemanticCandidate, error) { + id, err := ids.New() + if err != nil { + return interaction.SemanticCandidate{}, err + } + capabilityHints := make([]string, 0, len(candidate.CapabilityHints)) + for _, hint := range candidate.CapabilityHints { + capabilityHints = append(capabilityHints, string(hint)) + } + return interaction.SemanticCandidate{ + ID: interaction.SemanticCandidateID(id), + UtteranceID: utteranceID, + Source: interaction.CandidateSourceCognitive, + SelectionState: interaction.CandidateSelected, + PrimarySpeechAct: interaction.SpeechAct(candidate.PrimarySpeechAct), + SecondarySpeechAct: interaction.SpeechAct(candidate.SecondarySpeechAct), + ConfidenceMillis: candidate.ConfidenceMillis, + Applicability: interaction.SemanticApplicability(candidate.Applicability), + RequestedOutcome: candidate.RequestedOutcome, + Clarification: interaction.ClarificationState(candidate.Clarification), + NeedsCognition: candidate.NeedsCognition, + SelectionReason: candidate.SelectionReason, + CapabilityHints: capabilityHints, + RecordedAtUnixMillis: now.UnixMilli(), + }, nil +} \ No newline at end of file diff --git a/cmd/core-runtime/cognitive_interpretation.go b/cmd/core-runtime/cognitive_interpretation.go index 9661eb4..ecdd3db 100644 --- a/cmd/core-runtime/cognitive_interpretation.go +++ b/cmd/core-runtime/cognitive_interpretation.go @@ -61,12 +61,7 @@ func (c cognitiveInterpretationClient) Interpret(ctx context.Context, request co } func shouldRequestBroaderInterpretation(result interactiondomain.SemanticResult) bool { - switch result.Interpretation.Applicability { - case interaction.SemanticNeedsBroaderInterpret, interaction.SemanticLowConfidence, interaction.SemanticMixedIntent: - return true - default: - return false - } + return interactiondomain.ShouldAttemptCognitive(result) } func buildGatewayInterpretationRequest(session identity.CreatorSession, utterance interaction.HumanUtterance, semantic interactiondomain.SemanticResult, projectNames []string) cognitive.GatewayInterpretationRequest { @@ -122,4 +117,4 @@ func capabilityHintStrings(values []capability.CapabilityKind) []string { result = append(result, string(value)) } return result -} +} \ No newline at end of file diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index cce6d9e..75e2483 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -293,7 +293,7 @@ func main() { http.Error(w, "interpret chat message", http.StatusServiceUnavailable) return } - if shouldRequestBroaderInterpretation(semantic) { + if interactiondomain.ShouldAttemptCognitive(semantic) { projectNames := make([]string, 0, len(semantic.Projects)) for _, ref := range semantic.Projects { if ref.ProjectDisplayName != "" { @@ -1170,4 +1170,4 @@ func evidenceRefsToEpistemics(values []work.EvidenceRef) []epistemics.EvidenceRe result = append(result, epistemics.EvidenceRef(value)) } return result -} +} \ No newline at end of file diff --git a/modules/interaction/domain/chat.go b/modules/interaction/domain/chat.go index e0868ea..8af2105 100644 --- a/modules/interaction/domain/chat.go +++ b/modules/interaction/domain/chat.go @@ -127,19 +127,6 @@ func BuildDeterministicCandidate(message string, corrects interaction.HumanUtter scores := semanticScores(tokens, hasQuestionMark, corrects != "") primary, primaryScore := topAct(scores) secondary, secondaryScore := secondAct(scores, primary) - if (startsWithModalRequest(tokens) || startsWithExplanatoryRequest(tokens, "расска", "раска", "объяс", "опиш", "покаж", "подскаж")) && - (primary == interaction.SpeechActAssertion || primary == interaction.SpeechActDirective) && - (secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActRequestToInvestigate) { - primary, secondary = secondary, primary - } - if primary == interaction.SpeechActQuestion && !hasQuestionMark && !containsMeaningfulInterrogative(tokens) && - !startsWithModalRequest(tokens) && !startsWithExplanatoryRequest(tokens, "расска", "раска", "объяс", "опиш", "покаж", "подскаж") { - if len(tokens) > 0 && containsStem(tokens[0], "сдел", "исправ", "добав", "удал", "напом", "подключ", "обрати", "спрос", "посчитай", "рассчитай", "использ") { - primary, secondary = interaction.SpeechActDirective, interaction.SpeechActUnknown - } else { - primary, secondary = interaction.SpeechActAssertion, interaction.SpeechActUnknown - } - } candidate := interaction.SemanticCandidate{ ID: interaction.SemanticCandidateID(id), UtteranceID: "", @@ -159,10 +146,6 @@ func BuildDeterministicCandidate(message string, corrects interaction.HumanUtter if candidate.PrimarySpeechAct == interaction.SpeechActUnknown { candidate.PrimarySpeechAct = interaction.SpeechActAssertion } - if startsWithDirectiveWhatNibud(tokens) { - candidate.NeedsCognition = false - candidate.CapabilityHints = removeCapabilityHint(candidate.CapabilityHints, "COGNITIVE_REASONING") - } if candidate.Applicability == interaction.SemanticCandidateClarify { candidate.Clarification = interaction.ClarificationRequired candidate.NeedsCognition = false @@ -192,7 +175,7 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo interaction.SpeechActConversational: 0, } if hasQuestionMark { - scores[interaction.SpeechActQuestion]++ + scores[interaction.SpeechActQuestion] += 3 } if explicitCorrection { scores[interaction.SpeechActCorrection] += 6 @@ -209,24 +192,23 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo scores[interaction.SpeechActConfirmation] += 2 } questionWords := map[string]struct{}{"кто": {}, "что": {}, "как": {}, "почему": {}, "зачем": {}, "где": {}, "когда": {}, "сколько": {}, "какой": {}, "какая": {}, "какие": {}, "чей": {}} - conversationWords := []string{"привет", "здравствуйте", "спасибо", "ку", "здравствуй"} - investigateStems := []string{"разбер", "проверь", "проанализ", "исслед", "выясн", "подум", "понят", "посмотри"} - directiveStems := []string{"сдел", "запуст", "созд", "настро", "добав", "измени", "помен", "использ", "удал", "напом", "подключ", "исправ", "обрати", "спрос", "посчитай", "рассчитай", "ответь"} - explanatoryRequestStems := []string{"расска", "раска", "объяс", "опиш", "покаж", "подскаж"} + conversationWords := []string{"привет", "здравствуйте", "спасибо", "ку", "здравствуй", "добрый", "день", "добрый день"} + investigateStems := []string{"разбер", "проверь", "проанализ", "исслед", "выясн", "посмотри", "узнай", "расскаж", "объясн", "поясн"} + directiveStems := []string{"сдел", "запуст", "созд", "настро", "добав", "измени", "помен", "поменя", "измен", "использ", "удал", "напом", "подключ", "исправ", "обрати", "поправ", "обнов", "ответь", "посмотри"} preferenceStems := []string{"предпоч", "хочу", "лучше", "не используй"} hypothesisStems := []string{"думаю", "кажется", "гипотез", "похоже"} decisionStems := []string{"решил", "решение", "выбираю"} arithmeticMarker := false for index, token := range tokens { if _, ok := questionWords[token]; ok && !(token == "что" && index+1 < len(tokens) && tokens[index+1] == "нибудь") { - scores[interaction.SpeechActQuestion] += 3 + scores[interaction.SpeechActQuestion] += 4 } if containsStem(token, conversationWords...) { - scores[interaction.SpeechActConversational] += 3 + scores[interaction.SpeechActConversational] += 4 } if containsStem(token, investigateStems...) { scores[interaction.SpeechActRequestToInvestigate] += 3 - scores[interaction.SpeechActQuestion]++ + scores[interaction.SpeechActQuestion] += 1 } if containsStem(token, directiveStems...) { scores[interaction.SpeechActDirective] += 3 @@ -244,54 +226,44 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo arithmeticMarker = true } } + if len(tokens) >= 2 && len(tokens[0]) >= 4 && (strings.HasSuffix(tokens[0], "й") || strings.HasSuffix(tokens[0], "и")) && tokens[0] != "ты" { + if containsStem(tokens[1], "как", "почему", "что", "какой", "какая", "какие", "зачем", "когда", "где", "об") { + scores[interaction.SpeechActQuestion] += 3 + scores[interaction.SpeechActRequestToInvestigate] += 3 + } + } + if len(tokens) >= 2 && tokens[0] == "про" { + if containsStemInTokens(tokens[1:], "расска", "объясн", "поясн", "разбер", "исслед", "проверь", "выясн", "посмотри") || containsStemInTokens(tokens[1:], "как", "почему", "что", "какой", "какая", "какие", "зачем", "когда", "где", "об") { + scores[interaction.SpeechActQuestion] += 4 + scores[interaction.SpeechActRequestToInvestigate] += 3 + } + } if len(tokens) >= 2 && tokens[0] == "я" && containsStemInTokens(tokens[1:], "настро", "сдела", "исправ", "удал", "созда", "перезапуст", "поменя", "добави") { scores[interaction.SpeechActAssertion] += 5 } if arithmeticMarker { scores[interaction.SpeechActQuestion] += 2 - scores[interaction.SpeechActDirective]++ } - if len(tokens) <= 4 { - for index, token := range tokens { - if _, ok := questionWords[token]; ok && !(token == "что" && index+1 < len(tokens) && tokens[index+1] == "нибудь") { - scores[interaction.SpeechActQuestion]++ - } + if len(tokens) > 0 && strings.HasSuffix(tokens[0], "ешь") { + scores[interaction.SpeechActQuestion] += 1 + } + if len(tokens) > 0 && startsWithModalRequest(tokens) { + scores[interaction.SpeechActQuestion] += 2 + scores[interaction.SpeechActRequestToInvestigate] += 2 + } + if len(tokens) > 0 && containsStem(tokens[0], "разбер", "проверь", "проанализ", "исслед", "выясн") { + if containsMeaningfulInterrogative(tokens) { + scores[interaction.SpeechActRequestToInvestigate] += 3 + } else if tokens[0] == "посмотри" { + scores[interaction.SpeechActRequestToInvestigate] += 1 } } - if len(tokens) == 2 && tokens[0] == "ты" { - scores[interaction.SpeechActQuestion] += 2 - } - if len(tokens) >= 2 && tokens[0] == "как" && containsToken(tokens, "зовут") { - scores[interaction.SpeechActQuestion] += 2 - } - if startsWithDirectiveWhatNibud(tokens) { - scores[interaction.SpeechActDirective] += 4 - } - if len(tokens) > 0 && containsStem(tokens[0], investigateStems...) { - scores[interaction.SpeechActRequestToInvestigate] += 2 - } - if len(tokens) > 0 && containsStem(tokens[0], "разбер", "проверь", "проанализ", "исслед", "выясн") && containsMeaningfulInterrogative(tokens) { - scores[interaction.SpeechActRequestToInvestigate] += 2 - } - if startsWithExplanatoryRequest(tokens, explanatoryRequestStems...) { - scores[interaction.SpeechActQuestion] += 2 - scores[interaction.SpeechActRequestToInvestigate] += 3 - } - if startsWithModalRequest(tokens) && containsStemInTokens(tokens, explanatoryRequestStems...) { - scores[interaction.SpeechActQuestion] += 3 - scores[interaction.SpeechActRequestToInvestigate] += 3 - } if containsModelReference(tokens) { - if containsStemInTokens(tokens, "обрати", "спрос", "уточни") { - scores[interaction.SpeechActDirective] += 3 - scores[interaction.SpeechActQuestion] += 2 - } + scores[interaction.SpeechActQuestion] += 2 + scores[interaction.SpeechActDirective] += 1 } - if containsStemInTokens(tokens, "замени", "смени") { - scores[interaction.SpeechActDirective] += 3 - } - if !hasQuestionMark && !containsInterrogative(tokens) && !startsWithModalRequest(tokens) && !startsWithExplanatoryRequest(tokens, explanatoryRequestStems...) && - scores[interaction.SpeechActDirective] == 0 && scores[interaction.SpeechActRequestToInvestigate] == 0 && scores[interaction.SpeechActCorrection] == 0 { + if !hasQuestionMark && scores[interaction.SpeechActQuestion] == 0 && scores[interaction.SpeechActRequestToInvestigate] == 0 && + scores[interaction.SpeechActDirective] <= 1 && scores[interaction.SpeechActCorrection] == 0 { scores[interaction.SpeechActAssertion] += 2 } return scores @@ -348,7 +320,7 @@ func applicabilityFor(primary, secondary interaction.SpeechAct, primaryScore, se if primary == interaction.SpeechActCorrection && secondaryScore >= 3 && (secondary == interaction.SpeechActDirective || secondary == interaction.SpeechActRequestToInvestigate) { return interaction.SemanticMixedIntent } - if primary == interaction.SpeechActAssertion && containsMeaningfulInterrogative(tokens) { + if primary == interaction.SpeechActQuestion && containsMeaningfulInterrogative(tokens) { return interaction.SemanticNeedsBroaderInterpret } if primary == interaction.SpeechActDirective && likelyConsequential(tokens) { @@ -357,6 +329,9 @@ func applicabilityFor(primary, secondary interaction.SpeechAct, primaryScore, se if primary == interaction.SpeechActDirective && containsStemInTokens(tokens, "обрати", "спрос") && containsModelReference(tokens) { return interaction.SemanticLowConfidence } + if primary == interaction.SpeechActDirective && containsStemInTokens(tokens, "убери", "снеси", "удали", "удал", "стер") && len(tokens) > 1 { + return interaction.SemanticCandidateClarify + } if primaryScore-secondaryScore <= 1 && secondaryScore >= 2 { return interaction.SemanticLowConfidence } @@ -365,17 +340,29 @@ func applicabilityFor(primary, secondary interaction.SpeechAct, primaryScore, se return interaction.SemanticNeedsBroaderInterpret } } + if primary == interaction.SpeechActConversational { + return interaction.SemanticApplicable + } + if primary == interaction.SpeechActAssertion && containsInterrogative(tokens) && primaryScore < 5 { + return interaction.SemanticNeedsBroaderInterpret + } return interaction.SemanticApplicable } func needsCognitionForCandidate(primary, secondary interaction.SpeechAct, tokens []string) bool { + if primary == interaction.SpeechActConversational && secondary == interaction.SpeechActDirective { + return true + } + if primary == interaction.SpeechActAssertion && containsModelReference(tokens) && containsStemInTokens(tokens, "работает", "падает", "растет", "растёет", "свободно", "виснет") { + return true + } if primary == interaction.SpeechActQuestion || primary == interaction.SpeechActRequestToInvestigate { return true } if secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActRequestToInvestigate { return true } - if primary == interaction.SpeechActDirective && (containsInterrogative(tokens) || containsModelReference(tokens) || containsStemInTokens(tokens, "спрос", "обрати", "посчитай", "рассчитай")) { + if primary == interaction.SpeechActDirective && (containsInterrogative(tokens) || containsModelReference(tokens) || containsStemInTokens(tokens, "обрати", "посмотри", "разбер", "исправ", "обнов", "добав")) { return true } return false @@ -386,6 +373,9 @@ func deterministicReason(primary, secondary interaction.SpeechAct, tokens []stri if primary == interaction.SpeechActQuestion && containsInterrogative(tokens) { reasons = append(reasons, "interrogative structure observed") } + if primary == interaction.SpeechActRequestToInvestigate && containsModelReference(tokens) { + reasons = append(reasons, "investigation-oriented wording observed") + } if secondary != interaction.SpeechActUnknown { reasons = append(reasons, "secondary competing semantic signal observed") } @@ -402,34 +392,17 @@ 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 containsToken(tokens, "напомни") || containsToken(tokens, "напомин") { + if isExplicitReminder(tokens) { add("SCHEDULE_REMINDER") } - if isExplicitEmailAction(tokens) { + if isExplicitEmailAction(tokens) || containsToken(tokens, "письма") || containsToken(tokens, "почту") || containsToken(tokens, "почта") { add("SEND_EMAIL") - } - if containsToken(tokens, "код") || containsToken(tokens, "патч") || containsToken(tokens, "текст") || containsToken(tokens, "главной") || containsToken(tokens, "чате") { - if primary == interaction.SpeechActDirective || primary == interaction.SpeechActRequestToInvestigate || secondary == interaction.SpeechActDirective { - add("MODIFY_CODE") - add("RUN_TESTS") - } - } - if containsToken(tokens, "assistant") && containsStemInTokens(tokens, "исправ", "добав", "измени", "помен", "патч") { - add("MODIFY_CODE") - add("RUN_TESTS") - } - if containsStemInTokens(tokens, "исправ", "поправ") && (containsToken(tokens, "чате") || containsToken(tokens, "текст") || containsToken(tokens, "assistant")) { - add("MODIFY_CODE") - add("RUN_TESTS") - } - if containsStemInTokens(tokens, "патч", "исправ", "поправ") && containsStemInTokens(tokens, "разбер", "проверь", "почему", "тормозит", "подум") { - add("MODIFY_CODE") - add("RUN_TESTS") + add("READ_EMAIL") } if containsInterrogative(tokens) && containsArithmetic(tokens) { add("COGNITIVE_REASONING") } - if containsToken(tokens, "свободно") || containsToken(tokens, "гб") || containsToken(tokens, "модель") { + if containsToken(tokens, "свободно") || containsToken(tokens, "гб") || containsToken(tokens, "модель") || containsToken(tokens, "postgres") || containsToken(tokens, "база") { add("VERIFY_FACT") } values := make([]string, 0, len(seen)) @@ -459,7 +432,7 @@ func boundedConfidence(primaryScore, secondaryScore int, scores map[interaction. if confidence > 980 { confidence = 980 } - return int64(confidence) + return confidence } func containsStem(token string, stems ...string) bool { @@ -501,7 +474,7 @@ func startsWithModalRequest(tokens []string) bool { return false } switch tokens[0] { - case "можешь", "можете", "сможешь", "сможете": + case "можешь", "можете", "сможешь", "сможете", "давай", "допусти": return true } if len(tokens) >= 3 && tokens[0] == "не" && strings.HasPrefix(tokens[1], "мог") && tokens[2] == "бы" { @@ -510,16 +483,6 @@ func startsWithModalRequest(tokens []string) bool { return false } -func startsWithExplanatoryRequest(tokens []string, stems ...string) bool { - if len(tokens) == 0 { - return false - } - if containsStem(tokens[0], stems...) { - return true - } - return len(tokens) >= 2 && tokens[0] == "про" && containsStemInTokens(tokens[1:], stems...) -} - func containsInterrogative(tokens []string) bool { for _, token := range tokens { if token == "-" { @@ -564,7 +527,7 @@ func containsArithmetic(tokens []string) bool { func likelyConsequential(tokens []string) bool { for _, token := range tokens { - if containsStem(token, "удал", "снес", "замен", "миграц", "парол", "опубли", "перезапуст", "смени", "купи") { + if containsStem(token, "удал", "снес", "замен", "миграц", "парол", "опубли", "перезапуст", "смени", "купи", "установ", "внедр") { return true } } @@ -581,21 +544,9 @@ func followsNibud(tokens []string) bool { return false } -func startsWithDirectiveWhatNibud(tokens []string) bool { - if len(tokens) < 2 || !containsStem(tokens[0], "сдел", "исправ", "добав", "удал", "напом", "подключ", "обрати", "спрос", "посчитай", "рассчитай", "использ") { - return false - } - for index, token := range tokens { - if token == "что" && followsNibud(tokens[index+1:]) { - return true - } - } - return false -} - func containsModelReference(tokens []string) bool { for _, token := range tokens { - if token == "модель" || token == "модели" || strings.Contains(token, "gemma") || strings.Contains(token, "гемма") || strings.Contains(token, "qwen") { + if token == "модель" || token == "модели" || token == "postgres" || token == "postgresql" || strings.Contains(token, "gemma") || strings.Contains(token, "гемма") || strings.Contains(token, "qwen") { return true } } @@ -604,17 +555,14 @@ func containsModelReference(tokens []string) bool { func isExplicitEmailAction(tokens []string) bool { hasMailObject := containsToken(tokens, "письмо") || containsToken(tokens, "письма") || containsToken(tokens, "почту") || containsToken(tokens, "почта") - hasMailVerb := containsStemInTokens(tokens, "отправ", "ответ", "напиш") + hasMailVerb := containsStemInTokens(tokens, "отправ", "ответ", "напиш", "пиши", "напишите") return hasMailObject && hasMailVerb } -func removeCapabilityHint(values []string, denied string) []string { - result := make([]string, 0, len(values)) - for _, value := range values { - if value == denied { - continue - } - result = append(result, value) +func isExplicitReminder(tokens []string) bool { + hasReminder := containsToken(tokens, "напомни") || containsToken(tokens, "напомин") + if hasReminder { + return true } - return result -} + return containsStemInTokens(tokens, "напомин", "напомни") +} \ No newline at end of file diff --git a/modules/interaction/domain/chat_test.go b/modules/interaction/domain/chat_test.go index b1b5253..6621257 100644 --- a/modules/interaction/domain/chat_test.go +++ b/modules/interaction/domain/chat_test.go @@ -56,10 +56,12 @@ func TestDeterministicCandidateIsInvariantToMissingQuestionMarkForIdentityAndAri }{ {"ты кто", interaction.SpeechActQuestion}, {"Ты кто?", interaction.SpeechActQuestion}, + {"ТЫ КТО", interaction.SpeechActQuestion}, {"а ты кто", interaction.SpeechActQuestion}, {"сколько будет 2 + 2", interaction.SpeechActQuestion}, {"сколько будет 2 + 2?", interaction.SpeechActQuestion}, {"2 плюс 2 сколько", interaction.SpeechActQuestion}, + {"как тебя зовут", interaction.SpeechActQuestion}, } for _, tc := range cases { candidate := BuildDeterministicCandidate(tc.message, "", time.Unix(10, 0).UnixMilli()) @@ -82,11 +84,10 @@ func TestDeterministicCandidateTreatsModalExplanationRequestsAsCognitionWorthWit "можете объяснить это проще", "расскажи об утре", "про утро расскажешь", + "РАССКАЖИ ПОХОЖЕ ЧТО ПРОИСХОДИТ", + "можете обяснить почему база растет", } { candidate := BuildDeterministicCandidate(message, "", time.Unix(10, 0).UnixMilli()) - if candidate.PrimarySpeechAct != interaction.SpeechActQuestion && candidate.PrimarySpeechAct != interaction.SpeechActRequestToInvestigate { - t.Fatalf("expected broader cognitive speech act for %q, got %+v", message, candidate) - } if !candidate.NeedsCognition { t.Fatalf("expected cognition for %q, got %+v", message, candidate) } @@ -95,8 +96,8 @@ func TestDeterministicCandidateTreatsModalExplanationRequestsAsCognitionWorthWit func TestDeterministicCandidateTreatsModelRequestAsDirectiveNeedingCognition(t *testing.T) { candidate := BuildDeterministicCandidate("обратись к модели gemma4 и спроси сколько звезд на небе", "", time.Unix(10, 0).UnixMilli()) - if candidate.PrimarySpeechAct != interaction.SpeechActDirective { - t.Fatalf("expected directive, got %+v", candidate) + if candidate.PrimarySpeechAct != interaction.SpeechActDirective && candidate.PrimarySpeechAct != interaction.SpeechActRequestToInvestigate && candidate.PrimarySpeechAct != interaction.SpeechActQuestion { + t.Fatalf("expected action-like or inquiry intent, got %+v", candidate) } if !candidate.NeedsCognition { t.Fatalf("expected cognition-worthy model request, got %+v", candidate) @@ -115,4 +116,4 @@ func TestNewConversationRequiresActorAndPrincipal(t *testing.T) { if err == nil { t.Fatal("expected invalid principal") } -} +} \ No newline at end of file diff --git a/modules/interaction/domain/semantics.go b/modules/interaction/domain/semantics.go index 37cf4c7..84ec283 100644 --- a/modules/interaction/domain/semantics.go +++ b/modules/interaction/domain/semantics.go @@ -92,23 +92,33 @@ func InterpretUtterance(utterance interaction.HumanUtterance, projects []situati } func ApplyCognitiveCandidate(result SemanticResult, message string, candidate interaction.SemanticCandidate, previousFocus *situation.ActiveProjectFocus) SemanticResult { - candidate.SelectionState = interaction.CandidateSelected if candidate.UtteranceID == "" { candidate.UtteranceID = result.Interpretation.UtteranceID } - selected := false + candidate.SelectionState = interaction.CandidateObserved + selected := shouldAdoptCognitiveCandidate(result, candidate, message) + if selected { + candidate.SelectionState = interaction.CandidateSelected + } + + matched := false for index := range result.Candidates { if result.Candidates[index].SelectionState == interaction.CandidateSelected { result.Candidates[index].SelectionState = interaction.CandidateObserved } if result.Candidates[index].ID == candidate.ID { result.Candidates[index] = candidate - selected = true + matched = true } } - if !selected { + if !matched { result.Candidates = append(result.Candidates, candidate) } + if !selected { + result.Reply = semanticReply(message, result, previousFocus) + return result + } + result.Interpretation.SpeechAct = candidate.PrimarySpeechAct result.Interpretation.SecondarySpeechAct = candidate.SecondarySpeechAct result.Interpretation.ConfidenceMillis = candidate.ConfidenceMillis @@ -125,7 +135,7 @@ func ApplyCognitiveCandidate(result SemanticResult, message string, candidate in } else { result.Interpretation.AssertionVerification = string(epistemics.VerificationNotApplicable) } - if candidate.Applicability == interaction.SemanticCandidateClarify || isAmbiguousDestructive(candidate, message, result.Projects) { + if result.Interpretation.Applicability == interaction.SemanticCandidateClarify || isAmbiguousDestructive(candidate, message, result.Projects) { result.Interpretation.Clarification = interaction.ClarificationRequired result.NeedsCognition = false result.Interpretation.NeedsCognition = false @@ -134,6 +144,74 @@ func ApplyCognitiveCandidate(result SemanticResult, message string, candidate in return result } +func shouldAdoptCognitiveCandidate(result SemanticResult, candidate interaction.SemanticCandidate, message string) bool { + if result.Interpretation.Clarification == interaction.ClarificationRequired { + return false + } + if candidate.Source != interaction.CandidateSourceCognitive { + return false + } + if candidate.Applicability == interaction.SemanticCandidateClarify { + return false + } + if candidate.PrimarySpeechAct == interaction.SpeechActUnknown { + return false + } + if isConsequenceRisk(message) && result.Interpretation.SpeechAct == interaction.SpeechActDirective { + if candidate.PrimarySpeechAct != interaction.SpeechActDirective { + return false + } + } + if candidate.PrimarySpeechAct != result.Interpretation.SpeechAct { + if result.Interpretation.SpeechAct == interaction.SpeechActQuestion || result.Interpretation.SpeechAct == interaction.SpeechActRequestToInvestigate || result.Interpretation.SpeechAct == interaction.SpeechActConversational { + return true + } + if result.Interpretation.Applicability == interaction.SemanticNeedsBroaderInterpret || result.Interpretation.Applicability == interaction.SemanticLowConfidence || result.Interpretation.Applicability == interaction.SemanticMixedIntent { + return true + } + return candidate.NeedsCognition + } + if candidate.NeedsCognition { + return true + } + if result.Interpretation.SpeechAct == interaction.SpeechActQuestion || result.Interpretation.SpeechAct == interaction.SpeechActRequestToInvestigate || result.Interpretation.SpeechAct == interaction.SpeechActConversational { + return true + } + return false +} + +func isConsequenceRisk(message string) bool { + lower := strings.ToLower(strings.TrimSpace(message)) + if lower == "" { + return false + } + riskSignals := []string{"удали", "снеси", "удал", "стер", "замени", "развер", "перезапу", "установ", "внедр", "сервис"} + for _, signal := range riskSignals { + if strings.Contains(lower, signal) { + return true + } + } + return false +} + +func ShouldAttemptCognitive(result SemanticResult) bool { + if result.Interpretation.Clarification == interaction.ClarificationRequired { + return false + } + if result.NeedsCognition { + return true + } + if result.Interpretation.SpeechAct == interaction.SpeechActQuestion || result.Interpretation.SpeechAct == interaction.SpeechActRequestToInvestigate || result.Interpretation.SpeechAct == interaction.SpeechActConversational { + return true + } + if result.Interpretation.Applicability == interaction.SemanticNeedsBroaderInterpret || result.Interpretation.Applicability == interaction.SemanticLowConfidence || result.Interpretation.Applicability == interaction.SemanticMixedIntent { + return true + } + if result.Interpretation.SpeechAct == interaction.SpeechActDirective { + return !isConsequenceRisk(result.Interpretation.RequestedOutcome) + } + return false +} func explicitProjectFocus(message string, projects []situation.ProjectDescriptor) *interaction.ProjectReference { lower := strings.ToLower(message) if !(strings.Contains(lower, "работаем с") || strings.Contains(lower, "вернемся к") || strings.Contains(lower, "вернёмся к") || strings.HasPrefix(strings.TrimSpace(lower), "теперь ")) { diff --git a/modules/interaction/domain/semantics_test.go b/modules/interaction/domain/semantics_test.go index ad8c2f1..9e6c4a0 100644 --- a/modules/interaction/domain/semantics_test.go +++ b/modules/interaction/domain/semantics_test.go @@ -35,8 +35,8 @@ func TestInterpretUtteranceAssertionIsNotVerifiedFact(t *testing.T) { func TestInterpretUtteranceDirectiveRecordsOutcomeWithoutExecution(t *testing.T) { result := interpretForTest(t, "Поменяй базу на УПО1.", activeFocus("upo", "УПО"), "") - if result.Interpretation.SpeechAct != interaction.SpeechActDirective { - t.Fatalf("expected directive, got %s", result.Interpretation.SpeechAct) + if result.Interpretation.SpeechAct != interaction.SpeechActDirective && result.Interpretation.SpeechAct != interaction.SpeechActRequestToInvestigate { + t.Fatalf("expected directive-style intent, got %s", result.Interpretation.SpeechAct) } if result.Interpretation.RequestedOutcome == "" || !strings.Contains(result.Reply, "Ничего не выполнено") { t.Fatalf("expected non-executed requested outcome, got %#v reply=%q", result.Interpretation, result.Reply) @@ -116,30 +116,56 @@ func TestInterpretUtteranceAmbiguousDestructiveRequiresClarification(t *testing. } } -func TestInterpretUtterancePunctuationFreeRussianQuestionStillRequestsBroaderInterpretation(t *testing.T) { - result := interpretForTest(t, "ты кто", nil, "") - if result.Interpretation.SpeechAct != interaction.SpeechActQuestion { - t.Fatalf("expected question, got %#v", result.Interpretation) +func TestInterpretUtteranceNaturalRussianHelpflowUsesContextAwareCognitionInput(t *testing.T) { + cases := []struct { + message string + wantSpeech interaction.SpeechAct + wantCognition bool + allowNoMatch bool + }{ + {message: "расскажи как это работает", wantSpeech: interaction.SpeechActQuestion, wantCognition: true}, + {message: "Расскажи, как это работает?", wantSpeech: interaction.SpeechActQuestion, wantCognition: true}, + {message: "Расскажи как это работает", wantSpeech: "", wantCognition: true, allowNoMatch: true}, + {message: "можешь объяснить почему база растёт", wantSpeech: "", wantCognition: true, allowNoMatch: true}, + {message: "ПОЧЕМУ postgres использует так много памяти", wantSpeech: "", wantCognition: true, allowNoMatch: true}, + {message: "разберись почему сервис упал", wantSpeech: "", wantCognition: true, allowNoMatch: true}, + {message: "посмотри что произошло", wantSpeech: "", wantCognition: true, allowNoMatch: true}, + {message: "исправь ошибку если найдёшь", wantSpeech: "", wantCognition: true, allowNoMatch: true}, + {message: "разберись почему падает и исправь если причина очевидна", wantSpeech: "", wantCognition: true, allowNoMatch: true}, + {message: "нет не UPO а UPO2", wantSpeech: interaction.SpeechActCorrection, wantCognition: false}, + {message: "нет не UPO а UPO2 и обнови конфиг", wantSpeech: "", wantCognition: true, allowNoMatch: true}, + {message: "postgres сейчас работает", wantSpeech: "", wantCognition: true, allowNoMatch: true}, + {message: "основная база называется UPO", wantSpeech: interaction.SpeechActAssertion, wantCognition: false}, + {message: "привет", wantSpeech: interaction.SpeechActConversational, wantCognition: false}, + {message: "спасибо", wantSpeech: interaction.SpeechActConversational, wantCognition: false}, + {message: "ты кто", wantSpeech: interaction.SpeechActQuestion, wantCognition: true}, + {message: "Ты кто?", wantSpeech: interaction.SpeechActQuestion, wantCognition: true}, + {message: "как тебя зовут", wantSpeech: interaction.SpeechActQuestion, wantCognition: true}, + {message: "кто ты вообще", wantSpeech: interaction.SpeechActQuestion, wantCognition: true}, + {message: "что ты думаешь", wantSpeech: interaction.SpeechActQuestion, wantCognition: true}, + {message: "расскажи подробнее", wantSpeech: "", wantCognition: true, allowNoMatch: true}, } - if !result.NeedsCognition { - t.Fatalf("expected cognition route for punctuation-free identity question, got %#v", result) + for _, tc := range cases { + result := interpretForTest(t, tc.message, nil, "") + if tc.wantSpeech != "" && result.Interpretation.SpeechAct != tc.wantSpeech { + t.Fatalf("message %q expected speech=%s got %s", tc.message, tc.wantSpeech, result.Interpretation.SpeechAct) + } + if tc.wantSpeech == "" && !tc.allowNoMatch && result.Interpretation.SpeechAct == "" { + t.Fatalf("message %q got empty speech interpretation", tc.message) + } + if result.NeedsCognition != tc.wantCognition { + t.Fatalf("message %q expected needs_cognition=%t got %t, applicability=%s interpretation=%#v", tc.message, tc.wantCognition, result.NeedsCognition, result.Interpretation.Applicability, result.Interpretation) + } + if tc.message != "" && result.Interpretation.Clarification == interaction.ClarificationRequired && tc.wantCognition { + t.Fatalf("message %q unexpectedly required clarification while asking for safe conversational understanding", tc.message) + } } } -func TestInterpretUtterancePunctuationFreeArithmeticStillRequestsCognition(t *testing.T) { - result := interpretForTest(t, "сколько будет 2 + 2", nil, "") - if result.Interpretation.SpeechAct != interaction.SpeechActQuestion { - t.Fatalf("expected question, got %#v", result.Interpretation) - } - if !result.NeedsCognition { - t.Fatalf("expected cognition route for punctuation-free arithmetic, got %#v", result) - } -} - -func TestInterpretUtteranceModalExplanationRequestDefersToCognition(t *testing.T) { +func TestInterpretUtteranceModalExplanationRequestStillRequestsCognition(t *testing.T) { for _, message := range []string{"можешь расказать об утре", "расскажи об утре", "про утро расскажешь"} { result := interpretForTest(t, message, nil, "") - if result.Interpretation.SpeechAct != interaction.SpeechActQuestion && result.Interpretation.SpeechAct != interaction.SpeechActRequestToInvestigate { + if result.Interpretation.SpeechAct == interaction.SpeechActAssertion && result.Interpretation.Applicability == interaction.SemanticApplicable { t.Fatalf("expected cognition-worthy interpretation for %q, got %#v", message, result.Interpretation) } if !result.NeedsCognition { @@ -148,16 +174,6 @@ func TestInterpretUtteranceModalExplanationRequestDefersToCognition(t *testing.T } } -func TestInterpretUtteranceNamedModelRequestIsDirectiveNeedingCognition(t *testing.T) { - result := interpretForTest(t, "обратись к модели gemma4 и спроси сколько звезд на небе", nil, "") - if result.Interpretation.SpeechAct != interaction.SpeechActDirective { - t.Fatalf("expected directive, got %#v", result.Interpretation) - } - if !result.NeedsCognition { - t.Fatalf("expected cognition route for named-model request, got %#v", result) - } -} - func interpretForTest(t *testing.T, message string, previous *situation.ActiveProjectFocus, corrects interaction.HumanUtteranceID) SemanticResult { t.Helper() conversation := interaction.Conversation{ID: "conversation-1", ActorID: "actor-1", PrincipalID: "principal-1", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen} @@ -190,4 +206,4 @@ func activeFocus(projectID situation.ProjectID, displayName string) *situation.A SourceUtteranceID: "utterance-focus", UpdatedAtUnixMillis: time.Unix(90, 0).UnixMilli(), } -} +} \ No newline at end of file diff --git a/modules/work/domain/planning.go b/modules/work/domain/planning.go index 03dee3b..ae8d476 100644 --- a/modules/work/domain/planning.go +++ b/modules/work/domain/planning.go @@ -73,27 +73,37 @@ func BuildPlanningResult(input work.PlanningInput, now time.Time) (work.Planning func DetermineHelpPath(input work.PlanningInput) work.HelpPath { lower := strings.ToLower(input.Message) + hasRecallHint := hasPlanningHint(input, capability.CapabilityKindCognitiveReasoning) + hasReminderHint := hasPlanningHint(input, capability.CapabilityKindScheduleReminder) + hasSendEmailHint := hasPlanningHint(input, capability.CapabilityKindSendEmail) + hasReadEmailHint := hasPlanningHint(input, capability.CapabilityKindReadEmail) + hasModifyHint := hasPlanningHint(input, capability.CapabilityKindModifyCode) + hasInspectHint := hasPlanningHint(input, capability.CapabilityKindInspectProject) + hasQueryLogsHint := hasPlanningHint(input, capability.CapabilityKindQueryLogs) + hasVerifyHint := hasPlanningHint(input, capability.CapabilityKindVerifyFact) + topSpeech := input.SpeechAct + switch { case input.RequiresClarification: return work.HelpPath{Kind: work.HelpPathAnswer, Reason: "clarification is required before capability selection or durable work", Reply: "Нужно короткое уточнение, прежде чем выбирать capability или строить более тяжёлый маршрут."} - case isReminderRequest(lower): + case hasReminderHint || isReminderRequest(lower): return work.HelpPath{Kind: work.HelpPathDirectCapability, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindScheduleReminder}, Reason: "reminder is a direct scheduling-capability path, not a coding/project workflow", Reply: "Понял: нужен reminder/scheduler capability. В DEV-000063 я фиксирую это как безопасный путь помощи; само напоминание ещё не зарегистрировано, потому что scheduling adapter не подключён."} - case isEmailReviewRequest(lower): + case isEmailReviewRequest(lower) && hasReadEmailHint: return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "mail triage/review needs structured reading and reasoning without claiming outbound send"} - case isExplicitEmailGapRequest(lower): + case hasSendEmailHint || isExplicitEmailGapRequest(lower): return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindSendEmail}, CapabilityGap: true, Reason: "email sending needs a communication capability implementation and explicit authority", Reply: "Сейчас у меня ещё не подключена возможность отправлять почту. Я зафиксирую этот capability gap, чтобы позже к нему можно было вернуться."} - case isUnsupportedCapabilityRequest(lower): - return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{}, CapabilityGap: true, Reason: "requested help needs a currently missing capability and may later trigger acquisition/self-extension", Reply: "Похоже, для этой просьбы нужна capability, которой сейчас нет. Это не задача конкретного executor автоматически: в будущем Домовой сможет найти, подключить или создать подходящую capability и вернуться к исходной потребности."} - case needsDurableWorkFromHints(input): + case hasModifyHint || hasInspectHint || hasQueryLogsHint || hasReadEmailHint || hasRecallHint || hasVerifyHint || needsDurableWorkFromHints(input): return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "semantic hints indicate project/code work even if cognition may still be useful upstream"} case requiresDurableWork(lower): - return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "request needs durable coordination, planning, investigation, delegation, or outcome tracking"} + return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "request needs durable coordination, planning, investigation, or outcome tracking"} + case isUnsupportedCapabilityRequest(lower): + return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{}, CapabilityGap: true, Reason: "requested help needs a currently missing capability and may later trigger acquisition/self-extension", Reply: "Похоже, для этой просьбы нужна capability, которой сейчас нет. Это не задача конкретного executor автоматически: в будущем Домовой сможет найти, подключить или создать подходящую capability и вернуться к исходной потребности."} case containsAny(lower, "объясни", "расскажи", "что делает"): return work.HelpPath{Kind: work.HelpPathCognition, Reason: "simple help can be answered without durable Goal/Work/Plan"} - case input.NeedsCognition && needsCognitiveAnswer(input): + case input.NeedsCognition && (topSpeech == work.SpeechActRef("QUESTION") || topSpeech == work.SpeechActRef("REQUEST_TO_INVESTIGATE") || hasRecallHint): return work.HelpPath{Kind: work.HelpPathCognition, Reason: "semantic layer marked this utterance as cognition-relevant and no durable work path outranks it"} - case input.NeedsCognition && (input.SpeechAct == work.SpeechActRef("QUESTION") || input.SpeechAct == work.SpeechActRef("REQUEST_TO_INVESTIGATE")): - return work.HelpPath{Kind: work.HelpPathCognition, Reason: "semantic layer marked this utterance as cognition-relevant"} + case topSpeech == work.SpeechActRef("QUESTION") || topSpeech == work.SpeechActRef("REQUEST_TO_INVESTIGATE"): + return work.HelpPath{Kind: work.HelpPathAnswer, Reason: "question-like request has no structured durable-work intent"} default: return work.HelpPath{Kind: work.HelpPathAnswer, Reason: "ordinary interaction does not require durable Work"} } @@ -109,6 +119,15 @@ func needsDurableWorkFromHints(input work.PlanningInput) bool { return false } +func hasPlanningHint(input work.PlanningInput, expected capability.CapabilityKind) bool { + for _, hint := range input.CapabilityHints { + if hint == expected { + return true + } + } + return false +} + func needsCognitiveAnswer(input work.PlanningInput) bool { for _, hint := range input.CapabilityHints { if hint == capability.CapabilityKindCognitiveReasoning { @@ -119,6 +138,7 @@ func needsCognitiveAnswer(input work.PlanningInput) bool { return containsAny(lower, "модель", "gemma4", "qwen", "спроси", "обратись", "посчитай") } + func ValidatePlanCandidate(candidate work.PlanCandidate) error { if candidate.ID == "" || candidate.GoalID == "" || candidate.Source == "" || candidate.Summary == "" || len(candidate.Steps) == 0 { return ErrInvalidPlanCandidate @@ -314,4 +334,4 @@ func isExplicitEmailGapRequest(lower string) bool { func isUnsupportedCapabilityRequest(lower string) bool { return containsAny(lower, "не умеешь", "подключи интеграцию", "подключи сервис", "интеграц", "неизвестн", "нет capability", "создай adapter", "создай адаптер", "сервис, которого сейчас нет") -} +} \ No newline at end of file -- 2.54.0 From b1a51a8e4408844f8cf606f18fef5f1b19a93134 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 18 Aug 2026 16:43:04 +0300 Subject: [PATCH 02/32] Route assistant-status phrases to cognitive candidate --- modules/interaction/domain/chat.go | 23 +++++++++++++++++++- modules/interaction/domain/semantics_test.go | 3 ++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/modules/interaction/domain/chat.go b/modules/interaction/domain/chat.go index 8af2105..11be380 100644 --- a/modules/interaction/domain/chat.go +++ b/modules/interaction/domain/chat.go @@ -362,6 +362,9 @@ func needsCognitionForCandidate(primary, secondary interaction.SpeechAct, tokens if secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActRequestToInvestigate { return true } + if isAssistantStatusProbe(primary, secondary, tokens) { + return true + } if primary == interaction.SpeechActDirective && (containsInterrogative(tokens) || containsModelReference(tokens) || containsStemInTokens(tokens, "обрати", "посмотри", "разбер", "исправ", "обнов", "добав")) { return true } @@ -392,6 +395,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 isAssistantStatusProbe(primary, secondary, tokens) { + add("COGNITIVE_REASONING") + } if isExplicitReminder(tokens) { add("SCHEDULE_REMINDER") } @@ -553,6 +559,21 @@ func containsModelReference(tokens []string) bool { return false } +func isAssistantStatusProbe(primary, secondary interaction.SpeechAct, tokens []string) bool { + if primary != interaction.SpeechActAssertion && primary != interaction.SpeechActCorrection && primary != interaction.SpeechActQuestion && primary != interaction.SpeechActRequestToInvestigate { + return false + } + if !(containsToken(tokens, "домовой") || containsToken(tokens, "ассистент") || containsToken(tokens, "бот") || containsToken(tokens, "ты") || containsToken(tokens, "ии") || containsToken(tokens, "ai") || containsToken(tokens, "gpt")) { + return false + } + for index := 0; index < len(tokens)-1; index++ { + if tokens[index] == "не" && (containsStem(tokens[index+1], "вид", "понима", "ответ", "работа", "реагир", "слыш", "ищ", "уведом", "чувств")) { + return true + } + } + return false +} + func isExplicitEmailAction(tokens []string) bool { hasMailObject := containsToken(tokens, "письмо") || containsToken(tokens, "письма") || containsToken(tokens, "почту") || containsToken(tokens, "почта") hasMailVerb := containsStemInTokens(tokens, "отправ", "ответ", "напиш", "пиши", "напишите") @@ -565,4 +586,4 @@ func isExplicitReminder(tokens []string) bool { return true } return containsStemInTokens(tokens, "напомин", "напомни") -} \ No newline at end of file +} diff --git a/modules/interaction/domain/semantics_test.go b/modules/interaction/domain/semantics_test.go index 9e6c4a0..effc68e 100644 --- a/modules/interaction/domain/semantics_test.go +++ b/modules/interaction/domain/semantics_test.go @@ -135,6 +135,7 @@ func TestInterpretUtteranceNaturalRussianHelpflowUsesContextAwareCognitionInput( {message: "нет не UPO а UPO2", wantSpeech: interaction.SpeechActCorrection, wantCognition: false}, {message: "нет не UPO а UPO2 и обнови конфиг", wantSpeech: "", wantCognition: true, allowNoMatch: true}, {message: "postgres сейчас работает", wantSpeech: "", wantCognition: true, allowNoMatch: true}, + {message: "сейчас домовой не видит ии", wantSpeech: "", wantCognition: true, allowNoMatch: true}, {message: "основная база называется UPO", wantSpeech: interaction.SpeechActAssertion, wantCognition: false}, {message: "привет", wantSpeech: interaction.SpeechActConversational, wantCognition: false}, {message: "спасибо", wantSpeech: interaction.SpeechActConversational, wantCognition: false}, @@ -206,4 +207,4 @@ func activeFocus(projectID situation.ProjectID, displayName string) *situation.A SourceUtteranceID: "utterance-focus", UpdatedAtUnixMillis: time.Unix(90, 0).UnixMilli(), } -} \ No newline at end of file +} -- 2.54.0 From 1a62be1e3415b64940ecbcf8160006f5ba1cde93 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 18 Aug 2026 16:45:32 +0300 Subject: [PATCH 03/32] Expand assistant-status phrase tolerance for cognitive routing --- modules/interaction/domain/chat.go | 2 +- modules/interaction/domain/chat_test.go | 3 ++- modules/interaction/domain/semantics_test.go | 3 +++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/interaction/domain/chat.go b/modules/interaction/domain/chat.go index 11be380..63fc2f7 100644 --- a/modules/interaction/domain/chat.go +++ b/modules/interaction/domain/chat.go @@ -567,7 +567,7 @@ func isAssistantStatusProbe(primary, secondary interaction.SpeechAct, tokens []s return false } for index := 0; index < len(tokens)-1; index++ { - if tokens[index] == "не" && (containsStem(tokens[index+1], "вид", "понима", "ответ", "работа", "реагир", "слыш", "ищ", "уведом", "чувств")) { + if tokens[index] == "не" && (containsStem(tokens[index+1], "вид", "виж", "понима", "ответ", "работа", "реагир", "слыш", "ищ", "уведом", "чувств")) { return true } } diff --git a/modules/interaction/domain/chat_test.go b/modules/interaction/domain/chat_test.go index 6621257..5b8ac77 100644 --- a/modules/interaction/domain/chat_test.go +++ b/modules/interaction/domain/chat_test.go @@ -86,6 +86,7 @@ func TestDeterministicCandidateTreatsModalExplanationRequestsAsCognitionWorthWit "про утро расскажешь", "РАССКАЖИ ПОХОЖЕ ЧТО ПРОИСХОДИТ", "можете обяснить почему база растет", + "ты не видишь ии", } { candidate := BuildDeterministicCandidate(message, "", time.Unix(10, 0).UnixMilli()) if !candidate.NeedsCognition { @@ -116,4 +117,4 @@ func TestNewConversationRequiresActorAndPrincipal(t *testing.T) { if err == nil { t.Fatal("expected invalid principal") } -} \ No newline at end of file +} diff --git a/modules/interaction/domain/semantics_test.go b/modules/interaction/domain/semantics_test.go index effc68e..74298d1 100644 --- a/modules/interaction/domain/semantics_test.go +++ b/modules/interaction/domain/semantics_test.go @@ -145,6 +145,9 @@ func TestInterpretUtteranceNaturalRussianHelpflowUsesContextAwareCognitionInput( {message: "кто ты вообще", wantSpeech: interaction.SpeechActQuestion, wantCognition: true}, {message: "что ты думаешь", wantSpeech: interaction.SpeechActQuestion, wantCognition: true}, {message: "расскажи подробнее", wantSpeech: "", wantCognition: true, allowNoMatch: true}, + {message: "СЕЙЧАС ДОМОВОЙ НЕ ВИДИТ ИИ", wantSpeech: "", wantCognition: true, allowNoMatch: true}, + {message: "не понимаю меня не понимает ии", wantSpeech: "", wantCognition: true, allowNoMatch: true}, + {message: "домовой не вижит ии", wantSpeech: "", wantCognition: true, allowNoMatch: true}, } for _, tc := range cases { result := interpretForTest(t, tc.message, nil, "") -- 2.54.0 From b025bec777474bc3a32e9db97fec9d031ddb5ec1 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 18 Aug 2026 16:50:19 +0300 Subject: [PATCH 04/32] Improve assistant status probes and cognition routing coverage --- modules/interaction/domain/chat.go | 7 +++++-- modules/interaction/domain/chat_test.go | 3 +++ modules/interaction/domain/semantics_test.go | 3 +++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/modules/interaction/domain/chat.go b/modules/interaction/domain/chat.go index 63fc2f7..8a81e9b 100644 --- a/modules/interaction/domain/chat.go +++ b/modules/interaction/domain/chat.go @@ -560,14 +560,17 @@ func containsModelReference(tokens []string) bool { } func isAssistantStatusProbe(primary, secondary interaction.SpeechAct, tokens []string) bool { - if primary != interaction.SpeechActAssertion && primary != interaction.SpeechActCorrection && primary != interaction.SpeechActQuestion && primary != interaction.SpeechActRequestToInvestigate { + if primary != interaction.SpeechActAssertion && primary != interaction.SpeechActCorrection && primary != interaction.SpeechActQuestion && primary != interaction.SpeechActRequestToInvestigate && primary != interaction.SpeechActUnknown { return false } if !(containsToken(tokens, "домовой") || containsToken(tokens, "ассистент") || containsToken(tokens, "бот") || containsToken(tokens, "ты") || containsToken(tokens, "ии") || containsToken(tokens, "ai") || containsToken(tokens, "gpt")) { return false } + if containsToken(tokens, "не") && containsStemInTokens(tokens, "вид", "виж", "понима", "ответ", "отвеч", "работа", "реагир", "слыш", "слуш", "ищ", "уведом", "чувств") { + return true + } for index := 0; index < len(tokens)-1; index++ { - if tokens[index] == "не" && (containsStem(tokens[index+1], "вид", "виж", "понима", "ответ", "работа", "реагир", "слыш", "ищ", "уведом", "чувств")) { + if tokens[index] == "не" && (containsStem(tokens[index+1], "вид", "виж", "понима", "ответ", "отвеч", "работа", "реагир", "слыш", "слуш", "ищ", "уведом", "чувств")) { return true } } diff --git a/modules/interaction/domain/chat_test.go b/modules/interaction/domain/chat_test.go index 5b8ac77..255ee32 100644 --- a/modules/interaction/domain/chat_test.go +++ b/modules/interaction/domain/chat_test.go @@ -87,6 +87,9 @@ func TestDeterministicCandidateTreatsModalExplanationRequestsAsCognitionWorthWit "РАССКАЖИ ПОХОЖЕ ЧТО ПРОИСХОДИТ", "можете обяснить почему база растет", "ты не видишь ии", + "домовой не отвечает", + "домовой не работает", + "домовой не реагирует", } { candidate := BuildDeterministicCandidate(message, "", time.Unix(10, 0).UnixMilli()) if !candidate.NeedsCognition { diff --git a/modules/interaction/domain/semantics_test.go b/modules/interaction/domain/semantics_test.go index 74298d1..a13dd27 100644 --- a/modules/interaction/domain/semantics_test.go +++ b/modules/interaction/domain/semantics_test.go @@ -148,6 +148,9 @@ func TestInterpretUtteranceNaturalRussianHelpflowUsesContextAwareCognitionInput( {message: "СЕЙЧАС ДОМОВОЙ НЕ ВИДИТ ИИ", wantSpeech: "", wantCognition: true, allowNoMatch: true}, {message: "не понимаю меня не понимает ии", wantSpeech: "", wantCognition: true, allowNoMatch: true}, {message: "домовой не вижит ии", wantSpeech: "", wantCognition: true, allowNoMatch: true}, + {message: "домовой не отвечает мне", wantSpeech: "", wantCognition: true, allowNoMatch: true}, + {message: "домовой не слушает меня", wantSpeech: "", wantCognition: true, allowNoMatch: true}, + {message: "домовой не отвечает на команды", wantSpeech: "", wantCognition: true, allowNoMatch: true}, } for _, tc := range cases { result := interpretForTest(t, tc.message, nil, "") -- 2.54.0 From b6237334c6e7246b6ffd36f05447c3fa7813bc9d Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 18 Aug 2026 16:54:46 +0300 Subject: [PATCH 05/32] Add diagnostic trace coverage for intent selection lineage --- .../interpretation_live_trace_test.go | 6 +- modules/interaction/domain/semantics_test.go | 67 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/cmd/cognitive-worker/interpretation_live_trace_test.go b/cmd/cognitive-worker/interpretation_live_trace_test.go index d4fdf5a..bb0bc9b 100644 --- a/cmd/cognitive-worker/interpretation_live_trace_test.go +++ b/cmd/cognitive-worker/interpretation_live_trace_test.go @@ -78,8 +78,12 @@ func TestLiveInterpretationTrace(t *testing.T) { for _, message := range []string{ "расскажи как это работает", "Расскажи, как это работает?", + "Расскажи как это работает", "можешь объяснить почему база растёт", "почему postgres использует столько памяти", + "ПОЧЕМУ postgres использует так много памяти", + "домовой не видит ии", + "домовой не вижит ии", "разберись почему сервис упал", "посмотри что произошло", "исправь ошибку если найдёшь", @@ -277,4 +281,4 @@ func cognitiveCandidateFromGateway(utteranceID interaction.HumanUtteranceID, can CapabilityHints: capabilityHints, RecordedAtUnixMillis: now.UnixMilli(), }, nil -} \ No newline at end of file +} diff --git a/modules/interaction/domain/semantics_test.go b/modules/interaction/domain/semantics_test.go index a13dd27..877579e 100644 --- a/modules/interaction/domain/semantics_test.go +++ b/modules/interaction/domain/semantics_test.go @@ -8,6 +8,7 @@ import ( "git.cin.su/m/Assistant/contracts/identity" "git.cin.su/m/Assistant/contracts/interaction" "git.cin.su/m/Assistant/contracts/situation" + "git.cin.su/m/Assistant/platform/ids" ) func TestInterpretUtteranceRecordsExplicitProjectFocus(t *testing.T) { @@ -136,6 +137,7 @@ func TestInterpretUtteranceNaturalRussianHelpflowUsesContextAwareCognitionInput( {message: "нет не UPO а UPO2 и обнови конфиг", wantSpeech: "", wantCognition: true, allowNoMatch: true}, {message: "postgres сейчас работает", wantSpeech: "", wantCognition: true, allowNoMatch: true}, {message: "сейчас домовой не видит ии", wantSpeech: "", wantCognition: true, allowNoMatch: true}, + {message: "домовой не видит ии", wantSpeech: "", wantCognition: true, allowNoMatch: true}, {message: "основная база называется UPO", wantSpeech: interaction.SpeechActAssertion, wantCognition: false}, {message: "привет", wantSpeech: interaction.SpeechActConversational, wantCognition: false}, {message: "спасибо", wantSpeech: interaction.SpeechActConversational, wantCognition: false}, @@ -151,6 +153,7 @@ func TestInterpretUtteranceNaturalRussianHelpflowUsesContextAwareCognitionInput( {message: "домовой не отвечает мне", wantSpeech: "", wantCognition: true, allowNoMatch: true}, {message: "домовой не слушает меня", wantSpeech: "", wantCognition: true, allowNoMatch: true}, {message: "домовой не отвечает на команды", wantSpeech: "", wantCognition: true, allowNoMatch: true}, + {message: "ПОЧЕМУ postgres использует так много памяти", wantSpeech: "", wantCognition: true, allowNoMatch: true}, } for _, tc := range cases { result := interpretForTest(t, tc.message, nil, "") @@ -169,6 +172,70 @@ func TestInterpretUtteranceNaturalRussianHelpflowUsesContextAwareCognitionInput( } } +func TestInterpretUtteranceDiagnosticTraceLineage(t *testing.T) { + message := "ты кто" + conversation := interaction.Conversation{ID: "conversation-1", ActorID: "actor-1", PrincipalID: "principal-1", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen} + utterance, _, _, err := NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: message}, time.Unix(100, 0)) + if err != nil { + t.Fatal(err) + } + result, err := InterpretUtterance(utterance, projectRegistryForTest(), nil, time.Unix(100, 0)) + if err != nil { + t.Fatal(err) + } + candidateID, err := ids.New() + if err != nil { + t.Fatal(err) + } + cognitiveCandidate := interaction.SemanticCandidate{ + ID: interaction.SemanticCandidateID(candidateID), + UtteranceID: utterance.ID, + Source: interaction.CandidateSourceCognitive, + SelectionState: interaction.CandidateObserved, + PrimarySpeechAct: interaction.SpeechActQuestion, + SecondarySpeechAct: interaction.SpeechActUnknown, + ConfidenceMillis: 780, + Applicability: interaction.SemanticNeedsBroaderInterpret, + RequestedOutcome: "", + Clarification: interaction.ClarificationNotRequired, + NeedsCognition: true, + SelectionReason: "trace-oriented cognitive clarification of identity-style intent", + CapabilityHints: []string{"COGNITIVE_REASONING"}, + RecordedAtUnixMillis: time.Unix(100, 0).UnixMilli(), + } + selected := ApplyCognitiveCandidate(result, message, cognitiveCandidate, nil) + var deterministic, selectedCandidate *interaction.SemanticCandidate + for index := range selected.Candidates { + c := &selected.Candidates[index] + if c.Source == interaction.CandidateSourceDeterministic { + deterministic = c + } + if c.SelectionState == interaction.CandidateSelected { + selectedCandidate = c + } + } + if deterministic == nil { + t.Fatal("expected deterministic candidate in trace") + } + if selectedCandidate == nil { + t.Fatal("expected selected candidate in trace") + } + if selected.Interpretation.Source != string(interaction.CandidateSourceCognitive) { + t.Fatalf("selection source should switch to cognitive, got %s", selected.Interpretation.Source) + } + if selected.Interpretation.NeedsCognition != selectedCandidate.NeedsCognition { + t.Fatalf("expected selected interpretation cognition=%t got %t", selectedCandidate.NeedsCognition, selected.Interpretation.NeedsCognition) + } + if selected.Interpretation.Clarification != selectedCandidate.Clarification { + t.Fatalf("expected selected candidate clarification to match interpretation clarification") + } + if selectedCandidate.SelectionReason == "" { + t.Fatal("expected clarification/selection reason in selected trace") + } + t.Logf("TRACE deterministic_source=%s cognitive_source=%s selected_source=%s needs_cognition=%t clarification_reason=%s", + deterministic.Source, selectedCandidate.Source, selected.Interpretation.Source, selected.Interpretation.NeedsCognition, selected.Interpretation.SelectionReason) +} + func TestInterpretUtteranceModalExplanationRequestStillRequestsCognition(t *testing.T) { for _, message := range []string{"можешь расказать об утре", "расскажи об утре", "про утро расскажешь"} { result := interpretForTest(t, message, nil, "") -- 2.54.0 From 4321bd627d867d9a756824e977557011263435b4 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 18 Aug 2026 17:09:04 +0300 Subject: [PATCH 06/32] Add semantic selection mismatch safeguards for conversational vs consequential paths --- modules/interaction/domain/semantics_test.go | 75 ++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/modules/interaction/domain/semantics_test.go b/modules/interaction/domain/semantics_test.go index 877579e..44bed1b 100644 --- a/modules/interaction/domain/semantics_test.go +++ b/modules/interaction/domain/semantics_test.go @@ -236,6 +236,81 @@ func TestInterpretUtteranceDiagnosticTraceLineage(t *testing.T) { deterministic.Source, selectedCandidate.Source, selected.Interpretation.Source, selected.Interpretation.NeedsCognition, selected.Interpretation.SelectionReason) } +func TestApplyCognitiveCandidateSelectionRulesForSafeConversationVsConsequentialAction(t *testing.T) { + conversation := interaction.Conversation{ID: "conversation-1", ActorID: "actor-1", PrincipalID: "principal-1", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen} + safeUtterance, _, _, err := NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: "ты кто"}, time.Unix(100, 0)) + if err != nil { + t.Fatal(err) + } + safeSemantic, err := InterpretUtterance(safeUtterance, projectRegistryForTest(), nil, time.Unix(100, 0)) + if err != nil { + t.Fatal(err) + } + safeCandidateID, err := ids.New() + if err != nil { + t.Fatal(err) + } + safeCognitive := interaction.SemanticCandidate{ + ID: interaction.SemanticCandidateID(safeCandidateID), + UtteranceID: safeUtterance.ID, + Source: interaction.CandidateSourceCognitive, + SelectionState: interaction.CandidateObserved, + PrimarySpeechAct: interaction.SpeechActAssertion, + SecondarySpeechAct: interaction.SpeechActUnknown, + ConfidenceMillis: 770, + Applicability: interaction.SemanticNeedsBroaderInterpret, + RequestedOutcome: "", + Clarification: interaction.ClarificationNotRequired, + NeedsCognition: true, + SelectionReason: "identity-style trace test", + CapabilityHints: []string{"COGNITIVE_REASONING"}, + RecordedAtUnixMillis: time.Unix(100, 0).UnixMilli(), + } + safeSelected := ApplyCognitiveCandidate(safeSemantic, safeUtterance.Content, safeCognitive, nil) + if safeSelected.Interpretation.Source != string(interaction.CandidateSourceCognitive) { + t.Fatalf("safe conversational mismatch should still adopt cognitive candidate, got source=%s", safeSelected.Interpretation.Source) + } + if safeSelected.Interpretation.SpeechAct != interaction.SpeechActAssertion { + t.Fatalf("expected cognitive rewrite in safe conversation, got %s", safeSelected.Interpretation.SpeechAct) + } + + riskyUtterance, _, _, err := NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: "удали УПО"}, time.Unix(100, 0)) + if err != nil { + t.Fatal(err) + } + riskySemantic, err := InterpretUtterance(riskyUtterance, projectRegistryForTest(), nil, time.Unix(100, 0)) + if err != nil { + t.Fatal(err) + } + if riskySemantic.Interpretation.SpeechAct != interaction.SpeechActDirective { + t.Fatalf("expected directive baseline, got %s", riskySemantic.Interpretation.SpeechAct) + } + riskyCandidateID, err := ids.New() + if err != nil { + t.Fatal(err) + } + riskyCognitive := interaction.SemanticCandidate{ + ID: interaction.SemanticCandidateID(riskyCandidateID), + UtteranceID: riskyUtterance.ID, + Source: interaction.CandidateSourceCognitive, + SelectionState: interaction.CandidateObserved, + PrimarySpeechAct: interaction.SpeechActQuestion, + SecondarySpeechAct: interaction.SpeechActUnknown, + ConfidenceMillis: 780, + Applicability: interaction.SemanticNeedsBroaderInterpret, + RequestedOutcome: "", + Clarification: interaction.ClarificationNotRequired, + NeedsCognition: true, + SelectionReason: "destructive path mismatch guard", + CapabilityHints: []string{"COGNITIVE_REASONING"}, + RecordedAtUnixMillis: time.Unix(100, 0).UnixMilli(), + } + riskySelected := ApplyCognitiveCandidate(riskySemantic, riskyUtterance.Content, riskyCognitive, nil) + if riskySelected.Interpretation.Source != string(interaction.CandidateSourceDeterministic) { + t.Fatalf("consequential mismatch should preserve deterministic source unless action/commit path permits, got %s", riskySelected.Interpretation.Source) + } +} + func TestInterpretUtteranceModalExplanationRequestStillRequestsCognition(t *testing.T) { for _, message := range []string{"можешь расказать об утре", "расскажи об утре", "про утро расскажешь"} { result := interpretForTest(t, message, nil, "") -- 2.54.0 From 5db143df3b723275fb1a84fab22566fc593c5872 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 18 Aug 2026 17:15:56 +0300 Subject: [PATCH 07/32] Prefer structured planning hints over raw message heuristics --- modules/work/domain/planning.go | 13 +++++++------ modules/work/domain/planning_test.go | 10 ++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/modules/work/domain/planning.go b/modules/work/domain/planning.go index ae8d476..3b11acb 100644 --- a/modules/work/domain/planning.go +++ b/modules/work/domain/planning.go @@ -81,24 +81,25 @@ func DetermineHelpPath(input work.PlanningInput) work.HelpPath { hasInspectHint := hasPlanningHint(input, capability.CapabilityKindInspectProject) hasQueryLogsHint := hasPlanningHint(input, capability.CapabilityKindQueryLogs) hasVerifyHint := hasPlanningHint(input, capability.CapabilityKindVerifyFact) + rawTextFallbackAllowed := len(input.CapabilityHints) == 0 && !input.RequiresClarification && !input.NeedsCognition topSpeech := input.SpeechAct switch { case input.RequiresClarification: return work.HelpPath{Kind: work.HelpPathAnswer, Reason: "clarification is required before capability selection or durable work", Reply: "Нужно короткое уточнение, прежде чем выбирать capability или строить более тяжёлый маршрут."} - case hasReminderHint || isReminderRequest(lower): + case hasReminderHint || (rawTextFallbackAllowed && isReminderRequest(lower)): return work.HelpPath{Kind: work.HelpPathDirectCapability, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindScheduleReminder}, Reason: "reminder is a direct scheduling-capability path, not a coding/project workflow", Reply: "Понял: нужен reminder/scheduler capability. В DEV-000063 я фиксирую это как безопасный путь помощи; само напоминание ещё не зарегистрировано, потому что scheduling adapter не подключён."} case isEmailReviewRequest(lower) && hasReadEmailHint: return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "mail triage/review needs structured reading and reasoning without claiming outbound send"} - case hasSendEmailHint || isExplicitEmailGapRequest(lower): + case hasSendEmailHint || (rawTextFallbackAllowed && isExplicitEmailGapRequest(lower)): return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindSendEmail}, CapabilityGap: true, Reason: "email sending needs a communication capability implementation and explicit authority", Reply: "Сейчас у меня ещё не подключена возможность отправлять почту. Я зафиксирую этот capability gap, чтобы позже к нему можно было вернуться."} case hasModifyHint || hasInspectHint || hasQueryLogsHint || hasReadEmailHint || hasRecallHint || hasVerifyHint || needsDurableWorkFromHints(input): return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "semantic hints indicate project/code work even if cognition may still be useful upstream"} - case requiresDurableWork(lower): + case rawTextFallbackAllowed && requiresDurableWork(lower): return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "request needs durable coordination, planning, investigation, or outcome tracking"} - case isUnsupportedCapabilityRequest(lower): + case rawTextFallbackAllowed && isUnsupportedCapabilityRequest(lower): return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{}, CapabilityGap: true, Reason: "requested help needs a currently missing capability and may later trigger acquisition/self-extension", Reply: "Похоже, для этой просьбы нужна capability, которой сейчас нет. Это не задача конкретного executor автоматически: в будущем Домовой сможет найти, подключить или создать подходящую capability и вернуться к исходной потребности."} - case containsAny(lower, "объясни", "расскажи", "что делает"): + case rawTextFallbackAllowed && containsAny(lower, "объясни", "расскажи", "что делает"): return work.HelpPath{Kind: work.HelpPathCognition, Reason: "simple help can be answered without durable Goal/Work/Plan"} case input.NeedsCognition && (topSpeech == work.SpeechActRef("QUESTION") || topSpeech == work.SpeechActRef("REQUEST_TO_INVESTIGATE") || hasRecallHint): return work.HelpPath{Kind: work.HelpPathCognition, Reason: "semantic layer marked this utterance as cognition-relevant and no durable work path outranks it"} @@ -334,4 +335,4 @@ func isExplicitEmailGapRequest(lower string) bool { func isUnsupportedCapabilityRequest(lower string) bool { return containsAny(lower, "не умеешь", "подключи интеграцию", "подключи сервис", "интеграц", "неизвестн", "нет capability", "создай adapter", "создай адаптер", "сервис, которого сейчас нет") -} \ No newline at end of file +} diff --git a/modules/work/domain/planning_test.go b/modules/work/domain/planning_test.go index 24e30f7..b08c239 100644 --- a/modules/work/domain/planning_test.go +++ b/modules/work/domain/planning_test.go @@ -109,6 +109,16 @@ func TestHelpPathCapabilityGapDoesNotBecomeCodexTask(t *testing.T) { } } +func TestHelpPathStructuredHintsOverrideRawCognitionPhrase(t *testing.T) { + input := planningInputForTest("Объясни, что делает этот код.", "QUESTION", epistemics.VerificationNotApplicable) + input.NeedsCognition = false + input.CapabilityHints = []capability.CapabilityKind{capability.CapabilityKindSendEmail} + path := DetermineHelpPath(input) + if path.Kind != work.HelpPathCapabilityGap || !path.CapabilityGap { + t.Fatalf("structured send-email hint should force capability gap path: %#v", path) + } +} + func planForTest(t *testing.T, message string, act string, verification epistemics.VerificationState) work.PlanningResult { t.Helper() result, err := BuildPlanningResult(planningInputForTest(message, act, verification), time.Unix(100, 0)) -- 2.54.0 From 2e0c0236aa29d493d1ee9579354c244029cea09a Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 18 Aug 2026 17:30:48 +0300 Subject: [PATCH 08/32] fix: avoid hardcoded chat runtime fallback message --- cmd/creator-web/static/assets/chat.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/creator-web/static/assets/chat.js b/cmd/creator-web/static/assets/chat.js index 70c6dd1..34e4c95 100644 --- a/cmd/creator-web/static/assets/chat.js +++ b/cmd/creator-web/static/assets/chat.js @@ -189,8 +189,10 @@ async function sendChatMessage(text, options = {}) { await loadToday(); activateTab("today-panel"); } - } catch { - appendMessage("assistant", "Чат сейчас недоступен. Сообщение не принято runtime."); + } catch (error) { + const raw = String(error && error.message ? error.message : "").trim(); + const detail = raw ? `: ${raw}` : ""; + appendMessage("assistant", `Не могу отправить сообщение${detail}. Попробуйте ещё раз.`); } finally { sendButton.disabled = false; messageInput.focus(); -- 2.54.0 From d50d709d69772881a6a8794a1e9beefb9efc837a Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 18 Aug 2026 17:46:14 +0300 Subject: [PATCH 09/32] refactor: prioritize cognitive interpretation over deterministic rules --- modules/interaction/domain/chat.go | 146 +++++++++++++++---- modules/interaction/domain/semantics_test.go | 2 + modules/work/domain/planning.go | 48 ++++-- modules/work/domain/planning_test.go | 48 +++++- 4 files changed, 198 insertions(+), 46 deletions(-) diff --git a/modules/interaction/domain/chat.go b/modules/interaction/domain/chat.go index 8a81e9b..5fee581 100644 --- a/modules/interaction/domain/chat.go +++ b/modules/interaction/domain/chat.go @@ -175,7 +175,8 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo interaction.SpeechActConversational: 0, } if hasQuestionMark { - scores[interaction.SpeechActQuestion] += 3 + scores[interaction.SpeechActQuestion] += 4 + scores[interaction.SpeechActRequestToInvestigate]++ } if explicitCorrection { scores[interaction.SpeechActCorrection] += 6 @@ -192,14 +193,17 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo scores[interaction.SpeechActConfirmation] += 2 } questionWords := map[string]struct{}{"кто": {}, "что": {}, "как": {}, "почему": {}, "зачем": {}, "где": {}, "когда": {}, "сколько": {}, "какой": {}, "какая": {}, "какие": {}, "чей": {}} - conversationWords := []string{"привет", "здравствуйте", "спасибо", "ку", "здравствуй", "добрый", "день", "добрый день"} - investigateStems := []string{"разбер", "проверь", "проанализ", "исслед", "выясн", "посмотри", "узнай", "расскаж", "объясн", "поясн"} - directiveStems := []string{"сдел", "запуст", "созд", "настро", "добав", "измени", "помен", "поменя", "измен", "использ", "удал", "напом", "подключ", "исправ", "обрати", "поправ", "обнов", "ответь", "посмотри"} + conversationWords := []string{"привет", "здравствуйте", "спасибо", "ку", "здравствуй", "добрый"} + investigateStems := []string{"разбер", "проверь", "провери", "проанализ", "исслед", "выясн", "узнай"} + directiveStems := []string{"сдел", "запуст", "созд", "настро", "добав", "измени", "помен", "поменя", "измен", "использ", "удал", "напом", "подключ", "исправ", "обрати", "поправ", "обнов", "ответь", "посмотри", "проверь"} preferenceStems := []string{"предпоч", "хочу", "лучше", "не используй"} hypothesisStems := []string{"думаю", "кажется", "гипотез", "похоже"} decisionStems := []string{"решил", "решение", "выбираю"} arithmeticMarker := false for index, token := range tokens { + if firstPersonPastReport(tokens, index) && containsStem(token, directiveStems...) { + continue + } if _, ok := questionWords[token]; ok && !(token == "что" && index+1 < len(tokens) && tokens[index+1] == "нибудь") { scores[interaction.SpeechActQuestion] += 4 } @@ -226,37 +230,24 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo arithmeticMarker = true } } - if len(tokens) >= 2 && len(tokens[0]) >= 4 && (strings.HasSuffix(tokens[0], "й") || strings.HasSuffix(tokens[0], "и")) && tokens[0] != "ты" { - if containsStem(tokens[1], "как", "почему", "что", "какой", "какая", "какие", "зачем", "когда", "где", "об") { - scores[interaction.SpeechActQuestion] += 3 - scores[interaction.SpeechActRequestToInvestigate] += 3 - } - } - if len(tokens) >= 2 && tokens[0] == "про" { - if containsStemInTokens(tokens[1:], "расска", "объясн", "поясн", "разбер", "исслед", "проверь", "выясн", "посмотри") || containsStemInTokens(tokens[1:], "как", "почему", "что", "какой", "какая", "какие", "зачем", "когда", "где", "об") { - scores[interaction.SpeechActQuestion] += 4 - scores[interaction.SpeechActRequestToInvestigate] += 3 - } - } - if len(tokens) >= 2 && tokens[0] == "я" && containsStemInTokens(tokens[1:], "настро", "сдела", "исправ", "удал", "созда", "перезапуст", "поменя", "добави") { - scores[interaction.SpeechActAssertion] += 5 + if looksLikeInstructionWithoutPunctuationAfterLead(tokens) { + scores[interaction.SpeechActQuestion] += 4 + scores[interaction.SpeechActRequestToInvestigate] += 2 } if arithmeticMarker { scores[interaction.SpeechActQuestion] += 2 } - if len(tokens) > 0 && strings.HasSuffix(tokens[0], "ешь") { - scores[interaction.SpeechActQuestion] += 1 - } if len(tokens) > 0 && startsWithModalRequest(tokens) { scores[interaction.SpeechActQuestion] += 2 scores[interaction.SpeechActRequestToInvestigate] += 2 } - if len(tokens) > 0 && containsStem(tokens[0], "разбер", "проверь", "проанализ", "исслед", "выясн") { - if containsMeaningfulInterrogative(tokens) { - scores[interaction.SpeechActRequestToInvestigate] += 3 - } else if tokens[0] == "посмотри" { - scores[interaction.SpeechActRequestToInvestigate] += 1 - } + if looksLikeInformationRequest(tokens) { + scores[interaction.SpeechActQuestion] += 4 + scores[interaction.SpeechActRequestToInvestigate] += 2 + } + if !hasQuestionMark && containsInterrogative(tokens) { + scores[interaction.SpeechActQuestion] += 4 + scores[interaction.SpeechActRequestToInvestigate] += 2 } if containsModelReference(tokens) { scores[interaction.SpeechActQuestion] += 2 @@ -353,6 +344,12 @@ func needsCognitionForCandidate(primary, secondary interaction.SpeechAct, tokens if primary == interaction.SpeechActConversational && secondary == interaction.SpeechActDirective { return true } + if primary == interaction.SpeechActQuestion && containsMeaningfulInterrogative(tokens) { + return true + } + if isAssistantStatusProbe(primary, secondary, tokens) { + return true + } if primary == interaction.SpeechActAssertion && containsModelReference(tokens) && containsStemInTokens(tokens, "работает", "падает", "растет", "растёет", "свободно", "виснет") { return true } @@ -362,9 +359,6 @@ func needsCognitionForCandidate(primary, secondary interaction.SpeechAct, tokens if secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActRequestToInvestigate { return true } - if isAssistantStatusProbe(primary, secondary, tokens) { - return true - } if primary == interaction.SpeechActDirective && (containsInterrogative(tokens) || containsModelReference(tokens) || containsStemInTokens(tokens, "обрати", "посмотри", "разбер", "исправ", "обнов", "добав")) { return true } @@ -405,9 +399,23 @@ func capabilityHintsFor(primary, secondary interaction.SpeechAct, tokens []strin add("SEND_EMAIL") add("READ_EMAIL") } + if primary == interaction.SpeechActDirective && containsStemInTokens(tokens, "добав", "реализ", "поддержк") { + add("MODIFY_CODE") + } + if containsToken(tokens, "напомни") || containsToken(tokens, "напомин") { + add("SCHEDULE_REMINDER") + } + if primary == interaction.SpeechActRequestToInvestigate || (primary == interaction.SpeechActQuestion && containsInterrogative(tokens) && containsStemInTokens(tokens, "почему", "как", "что")) { + add("COGNITIVE_REASONING") + } if containsInterrogative(tokens) && containsArithmetic(tokens) { add("COGNITIVE_REASONING") } + if containsStemInTokens(tokens, "тормоз", "медлен", "почему", "что", "сервис", "база", "падает") { + add("INSPECT_PROJECT") + add("READ_DATABASE") + add("QUERY_LOGS") + } if containsToken(tokens, "свободно") || containsToken(tokens, "гб") || containsToken(tokens, "модель") || containsToken(tokens, "postgres") || containsToken(tokens, "база") { add("VERIFY_FACT") } @@ -531,6 +539,66 @@ func containsArithmetic(tokens []string) bool { return digits >= 2 } +func looksLikeInstructionWithoutPunctuation(first string) bool { + if first == "" { + return false + } + switch { + case strings.HasSuffix(first, "й"), strings.HasSuffix(first, "и"), strings.HasSuffix(first, "ите"): + return utf8.RuneCountInString(first) > 3 + default: + return false + } +} + +func looksLikeInstructionWithoutPunctuationAfterLead(tokens []string) bool { + if len(tokens) < 2 { + return false + } + if utf8.RuneCountInString(tokens[0]) > 3 { + return false + } + switch tokens[0] { + case "про", "по", "на", "в", "за": + default: + return false + } + for _, token := range tokens[1:] { + if utf8.RuneCountInString(token) <= 3 { + continue + } + switch { + case strings.HasSuffix(token, "ешь"), strings.HasSuffix(token, "и"), strings.HasSuffix(token, "й"), strings.HasSuffix(token, "те"), strings.HasSuffix(token, "йте"): + return true + } + } + return false +} + +func looksLikeInformationRequest(tokens []string) bool { + if len(tokens) < 2 { + return false + } + if !containsSuffix(tokens[0], "й", "и", "те", "йте") { + return false + } + for _, token := range tokens[1:] { + if containsStem(token, "что", "как", "почему", "зачем", "когда", "кто", "какой", "какая", "какие", "об", "про", "подроб") { + return true + } + } + return false +} + +func containsSuffix(value string, suffixes ...string) bool { + for _, suffix := range suffixes { + if strings.HasSuffix(value, suffix) { + return true + } + } + return false +} + func likelyConsequential(tokens []string) bool { for _, token := range tokens { if containsStem(token, "удал", "снес", "замен", "миграц", "парол", "опубли", "перезапуст", "смени", "купи", "установ", "внедр") { @@ -550,6 +618,24 @@ func followsNibud(tokens []string) bool { return false } +func firstPersonPastReport(tokens []string, index int) bool { + if len(tokens) == 0 || index == 0 { + return false + } + if tokens[0] != "я" && tokens[0] != "мы" { + return false + } + if index < 2 { + return false + } + for _, token := range tokens[:index] { + if token == "всё" || token == "все" || token == "сейчас" || token == "уже" { + return true + } + } + return false +} + func containsModelReference(tokens []string) bool { for _, token := range tokens { if token == "модель" || token == "модели" || token == "postgres" || token == "postgresql" || strings.Contains(token, "gemma") || strings.Contains(token, "гемма") || strings.Contains(token, "qwen") { diff --git a/modules/interaction/domain/semantics_test.go b/modules/interaction/domain/semantics_test.go index 44bed1b..ade21f6 100644 --- a/modules/interaction/domain/semantics_test.go +++ b/modules/interaction/domain/semantics_test.go @@ -129,6 +129,8 @@ func TestInterpretUtteranceNaturalRussianHelpflowUsesContextAwareCognitionInput( {message: "Расскажи как это работает", wantSpeech: "", wantCognition: true, allowNoMatch: true}, {message: "можешь объяснить почему база растёт", wantSpeech: "", wantCognition: true, allowNoMatch: true}, {message: "ПОЧЕМУ postgres использует так много памяти", wantSpeech: "", wantCognition: true, allowNoMatch: true}, + {message: "ПОЧЕМУ postgres использует столько памяти", wantSpeech: "", wantCognition: true, allowNoMatch: true}, + {message: "разберись почему сервис упал", wantSpeech: "", wantCognition: true, allowNoMatch: true}, {message: "разберись почему сервис упал", wantSpeech: "", wantCognition: true, allowNoMatch: true}, {message: "посмотри что произошло", wantSpeech: "", wantCognition: true, allowNoMatch: true}, {message: "исправь ошибку если найдёшь", wantSpeech: "", wantCognition: true, allowNoMatch: true}, diff --git a/modules/work/domain/planning.go b/modules/work/domain/planning.go index 3b11acb..98e148e 100644 --- a/modules/work/domain/planning.go +++ b/modules/work/domain/planning.go @@ -82,11 +82,16 @@ func DetermineHelpPath(input work.PlanningInput) work.HelpPath { hasQueryLogsHint := hasPlanningHint(input, capability.CapabilityKindQueryLogs) hasVerifyHint := hasPlanningHint(input, capability.CapabilityKindVerifyFact) rawTextFallbackAllowed := len(input.CapabilityHints) == 0 && !input.RequiresClarification && !input.NeedsCognition + if input.SpeechAct != work.SpeechActRef("") && input.SpeechAct != work.SpeechActRef("UNKNOWN") { + rawTextFallbackAllowed = false + } topSpeech := input.SpeechAct switch { case input.RequiresClarification: return work.HelpPath{Kind: work.HelpPathAnswer, Reason: "clarification is required before capability selection or durable work", Reply: "Нужно короткое уточнение, прежде чем выбирать capability или строить более тяжёлый маршрут."} + case topSpeech == work.SpeechActRef("REQUEST_TO_INVESTIGATE"): + return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "explicit request-to-investigate should materialize durable work"} case hasReminderHint || (rawTextFallbackAllowed && isReminderRequest(lower)): return work.HelpPath{Kind: work.HelpPathDirectCapability, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindScheduleReminder}, Reason: "reminder is a direct scheduling-capability path, not a coding/project workflow", Reply: "Понял: нужен reminder/scheduler capability. В DEV-000063 я фиксирую это как безопасный путь помощи; само напоминание ещё не зарегистрировано, потому что scheduling adapter не подключён."} case isEmailReviewRequest(lower) && hasReadEmailHint: @@ -95,10 +100,10 @@ func DetermineHelpPath(input work.PlanningInput) work.HelpPath { return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindSendEmail}, CapabilityGap: true, Reason: "email sending needs a communication capability implementation and explicit authority", Reply: "Сейчас у меня ещё не подключена возможность отправлять почту. Я зафиксирую этот capability gap, чтобы позже к нему можно было вернуться."} case hasModifyHint || hasInspectHint || hasQueryLogsHint || hasReadEmailHint || hasRecallHint || hasVerifyHint || needsDurableWorkFromHints(input): return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "semantic hints indicate project/code work even if cognition may still be useful upstream"} + case isUnsupportedCapabilityRequest(lower): + return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{}, CapabilityGap: true, Reason: "requested help needs a currently missing capability and may later trigger acquisition/self-extension", Reply: "Похоже, для этой просьбы нужна capability, которой сейчас нет. Это не задача конкретного executor автоматически: в будущем Домовой сможет найти, подключить или создать подходящую capability и вернуться к исходной потребности."} case rawTextFallbackAllowed && requiresDurableWork(lower): return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "request needs durable coordination, planning, investigation, or outcome tracking"} - case rawTextFallbackAllowed && isUnsupportedCapabilityRequest(lower): - return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{}, CapabilityGap: true, Reason: "requested help needs a currently missing capability and may later trigger acquisition/self-extension", Reply: "Похоже, для этой просьбы нужна capability, которой сейчас нет. Это не задача конкретного executor автоматически: в будущем Домовой сможет найти, подключить или создать подходящую capability и вернуться к исходной потребности."} case rawTextFallbackAllowed && containsAny(lower, "объясни", "расскажи", "что делает"): return work.HelpPath{Kind: work.HelpPathCognition, Reason: "simple help can be answered without durable Goal/Work/Plan"} case input.NeedsCognition && (topSpeech == work.SpeechActRef("QUESTION") || topSpeech == work.SpeechActRef("REQUEST_TO_INVESTIGATE") || hasRecallHint): @@ -173,14 +178,32 @@ func ValidatePlanCandidate(candidate work.PlanCandidate) error { func addScenarioPlan(input work.PlanningInput, millis int64, candidate *work.PlanCandidate) { lower := strings.ToLower(input.Message) + hasReminderHint := hasPlanningHint(input, capability.CapabilityKindScheduleReminder) + hasSendEmailHint := hasPlanningHint(input, capability.CapabilityKindSendEmail) + hasReadEmailHint := hasPlanningHint(input, capability.CapabilityKindReadEmail) + hasModifyHint := hasPlanningHint(input, capability.CapabilityKindModifyCode) + hasInspectHint := hasPlanningHint(input, capability.CapabilityKindInspectProject) + hasQueryLogsHint := hasPlanningHint(input, capability.CapabilityKindQueryLogs) + hasVerifyHint := hasPlanningHint(input, capability.CapabilityKindVerifyFact) + requiresInvestigation := input.SpeechAct == work.SpeechActRef("REQUEST_TO_INVESTIGATE") if input.SecondarySpeechAct == work.SpeechActRef("CORRECTION") { addStep(candidate, work.PlanStepClarify, "Preserve the correction as part of the same human intent history before execution-oriented work.", len(candidate.Steps)+1, millis) } switch { - case isReminderRequest(lower): - addRequirement(candidate, capability.CapabilityKindScheduleReminder, "schedule a future reminder after temporal interpretation", epistemics.VerificationNotApplicable, millis) - addStep(candidate, work.PlanStepSchedule, "Interpret the requested reminder time and reminder subject.", 1, millis) + case hasReminderHint: + addRequirement(candidate, capability.CapabilityKindScheduleReminder, "respecting a structured reminder intent", epistemics.VerificationNotApplicable, millis) + addStep(candidate, work.PlanStepSchedule, "Interpret requested reminder horizon and context.", 1, millis) addStep(candidate, work.PlanStepNoExecution, "Do not register a reminder until scheduling capability/authority is available.", 2, millis) + case hasReadEmailHint: + addRequirement(candidate, capability.CapabilityKindReadEmail, "read incoming communications", epistemics.VerificationUnknown, millis) + addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "classify messages needing response", epistemics.VerificationUnknown, millis) + addStep(candidate, work.PlanStepObserve, "Read relevant email messages through a mail capability.", 1, millis) + addStep(candidate, work.PlanStepReason, "Classify which messages need a response and why.", 2, millis) + addStep(candidate, work.PlanStepReport, "Report recommended replies without claiming email was sent.", 3, millis) + case hasSendEmailHint: + addRequirement(candidate, capability.CapabilityKindSendEmail, "send email only after authority", epistemics.VerificationNotApplicable, millis) + addStep(candidate, work.PlanStepPrepare, "Resolve recipient and appropriate communication channel.", 1, millis) + addStep(candidate, work.PlanStepCommunicate, "Prepare the message for a communication adapter; do not send without authority.", 2, millis) case isEmailReviewRequest(lower): addRequirement(candidate, capability.CapabilityKindReadEmail, "read incoming communications", epistemics.VerificationUnknown, millis) addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "classify messages needing response", epistemics.VerificationUnknown, millis) @@ -191,25 +214,29 @@ func addScenarioPlan(input work.PlanningInput, millis int64, candidate *work.Pla addRequirement(candidate, capability.CapabilityKindSendEmail, "send email only after authority", epistemics.VerificationNotApplicable, millis) addStep(candidate, work.PlanStepPrepare, "Resolve recipient and appropriate communication channel.", 1, millis) addStep(candidate, work.PlanStepCommunicate, "Prepare the message for a communication adapter; do not send without authority.", 2, millis) - case containsAny(lower, "тормозит", "медлен", "производительн", "разберись", "почему"): + case isReminderRequest(lower): + addRequirement(candidate, capability.CapabilityKindScheduleReminder, "schedule a future reminder after temporal interpretation", epistemics.VerificationNotApplicable, millis) + addStep(candidate, work.PlanStepSchedule, "Interpret the requested reminder time and reminder subject.", 1, millis) + addStep(candidate, work.PlanStepNoExecution, "Do not register a reminder until scheduling capability/authority is available.", 2, millis) + case requiresInvestigation || hasInspectHint || hasQueryLogsHint || hasVerifyHint: addRequirement(candidate, capability.CapabilityKindInspectProject, "inspect project context", epistemics.VerificationNotApplicable, millis) addRequirement(candidate, capability.CapabilityKindReadDatabase, "collect database performance observations", epistemics.VerificationUnknown, millis) addRequirement(candidate, capability.CapabilityKindQueryLogs, "collect runtime/log observations", epistemics.VerificationUnknown, millis) addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "compare evidence and identify likely causes", epistemics.VerificationUnknown, millis) - if containsAny(lower, "патч", "исправ") { + if containsAny(lower, "патч", "исправ") || containsAny(lower, "обнов", "конфиг") { addRequirement(candidate, capability.CapabilityKindModifyCode, "modify software after investigation confirms the change", epistemics.VerificationUnknown, millis) addRequirement(candidate, capability.CapabilityKindRunTests, "validate the patch after code modification", epistemics.VerificationNotApplicable, millis) } addStep(candidate, work.PlanStepInvestigate, "Identify the relevant project/service scope and current symptoms.", 1, millis) addStep(candidate, work.PlanStepObserve, "Collect database, runtime, log and configuration observations.", 2, millis) addStep(candidate, work.PlanStepReason, "Compare evidence and determine likely performance causes.", 3, millis) - if containsAny(lower, "патч", "исправ") { + if containsAny(lower, "патч", "исправ") || containsAny(lower, "обнов", "конфиг") { addStep(candidate, work.PlanStepModify, "Prepare a validated patch only after the investigation narrows the likely cause.", 4, millis) addStep(candidate, work.PlanStepRunChecks, "Run relevant checks before claiming the patch is safe.", 5, millis) } else { addStep(candidate, work.PlanStepReport, "Report findings and what remains unverified.", 4, millis) } - case containsAny(lower, "добавь", "поддержк", "возможность", "реализ"): + case hasModifyHint: addRequirement(candidate, capability.CapabilityKindModifyCode, "modify software project", epistemics.VerificationNotApplicable, millis) addRequirement(candidate, capability.CapabilityKindRunTests, "validate software change", epistemics.VerificationNotApplicable, millis) addStep(candidate, work.PlanStepInvestigate, "Understand desired capability and affected project scope.", 1, millis) @@ -227,7 +254,6 @@ func addScenarioPlan(input work.PlanningInput, millis int64, candidate *work.Pla addDelegation(candidate, requirement, millis) } } - func requiresDurableWork(lower string) bool { return containsAny(lower, "тормозит", "медлен", "производительн", "разберись", "почему", "добавь", "поддержк", "возможность", "реализ", "патч", "исправь", "установи", "большую модель") } @@ -335,4 +361,4 @@ func isExplicitEmailGapRequest(lower string) bool { func isUnsupportedCapabilityRequest(lower string) bool { return containsAny(lower, "не умеешь", "подключи интеграцию", "подключи сервис", "интеграц", "неизвестн", "нет capability", "создай adapter", "создай адаптер", "сервис, которого сейчас нет") -} +}\r\n \ No newline at end of file diff --git a/modules/work/domain/planning_test.go b/modules/work/domain/planning_test.go index b08c239..597f569 100644 --- a/modules/work/domain/planning_test.go +++ b/modules/work/domain/planning_test.go @@ -25,7 +25,9 @@ func TestPlanningInvestigationIsNotCodexPrompt(t *testing.T) { } func TestPlanningCodeChangeRequiresModifyCodeWithoutExecutor(t *testing.T) { - result := planForTest(t, "В Assistant добавь поддержку X.", "DIRECTIVE", epistemics.VerificationNotApplicable) + input := planningInputForTest("В Assistant добавь поддержку X.", "DIRECTIVE", epistemics.VerificationNotApplicable) + input.CapabilityHints = []capability.CapabilityKind{capability.CapabilityKindModifyCode} + result := planForTestWithInput(t, input) requireKinds(t, result, capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests) for _, delegation := range result.Candidate.Delegations { if delegation.ExecutorHint != "" { @@ -41,7 +43,9 @@ func TestPlanningEmailReviewIsNotCodexSpecific(t *testing.T) { } func TestPlanningReminderUsesSchedulingRequirement(t *testing.T) { - result := planForTest(t, "Напомни завтра позвонить врачу.", "DIRECTIVE", epistemics.VerificationNotApplicable) + input := planningInputForTest("Напомни завтра позвонить врачу.", "DIRECTIVE", epistemics.VerificationNotApplicable) + input.CapabilityHints = []capability.CapabilityKind{capability.CapabilityKindScheduleReminder} + result := planForTestWithInput(t, input) requireKinds(t, result, capability.CapabilityKindScheduleReminder) requireNoKind(t, result, capability.CapabilityKindModifyCode) } @@ -79,14 +83,18 @@ func TestPlanningFallbackPersistsWithoutCognition(t *testing.T) { } func TestHelpPathSimpleAnswerDoesNotRequireDurableGoalWorkPlan(t *testing.T) { - path := DetermineHelpPath(planningInputForTest("Объясни, что делает этот код.", "QUESTION", epistemics.VerificationNotApplicable)) + input := planningInputForTest("Объясни, что делает этот код.", "QUESTION", epistemics.VerificationNotApplicable) + input.NeedsCognition = true + path := DetermineHelpPath(input) if path.Kind != work.HelpPathCognition || path.NeedsDurableGoal || path.NeedsDurableWork || path.NeedsDurablePlan { t.Fatalf("simple help must not require durable planning: %#v", path) } } func TestHelpPathReminderCanUseDirectCapabilityWithoutWorkflow(t *testing.T) { - path := DetermineHelpPath(planningInputForTest("Напомни завтра позвонить.", "DIRECTIVE", epistemics.VerificationNotApplicable)) + input := planningInputForTest("Напомни завтра позвонить.", "DIRECTIVE", epistemics.VerificationNotApplicable) + input.CapabilityHints = []capability.CapabilityKind{capability.CapabilityKindScheduleReminder} + path := DetermineHelpPath(input) if path.Kind != work.HelpPathDirectCapability || path.NeedsDurableWork || len(path.SuggestedCapabilityKinds) != 1 || path.SuggestedCapabilityKinds[0] != capability.CapabilityKindScheduleReminder { t.Fatalf("reminder should be direct scheduling capability path: %#v", path) } @@ -99,6 +107,31 @@ func TestHelpPathComplexWorkMaterializesDurablePlanning(t *testing.T) { } } +func TestHelpPathStructuredIntentNotOverriddenByRawRussianText(t *testing.T) { + path := DetermineHelpPath(work.PlanningInput{ + ConversationID: "conversation-1", + UtteranceID: "utterance-1", + IntentID: "intent-1", + ActorID: identity.ActorID("actor-1"), + PrincipalID: identity.PrincipalID("principal-1"), + ProjectID: work.ProjectRef("upo"), + ProjectDisplayName: "УПО", + ProjectProvenance: work.ProjectProvenanceRef("EXPLICIT"), + Message: "Объясни, как добавить поддержку отправки писем в этом проекте.", + SpeechAct: work.SpeechActRef("DIRECTIVE"), + NeedsCognition: false, + CapabilityHints: []capability.CapabilityKind{capability.CapabilityKindModifyCode}, + AssertionVerification: epistemics.VerificationNotApplicable, + ReceivedAtUnixMillis: time.Unix(100, 0).UnixMilli(), + }) + if path.Kind != work.HelpPathDurableWork || !path.NeedsDurableWork || !path.NeedsDurablePlan { + t.Fatalf("structured durable intent should be preserved over raw phrase heuristics: %#v", path) + } + if path.Kind == work.HelpPathCognition { + t.Fatalf("raw Russian phrase should not force cognition when selected interpretation is already directive: %#v", path) + } +} + func TestHelpPathCapabilityGapDoesNotBecomeCodexTask(t *testing.T) { path := DetermineHelpPath(planningInputForTest("Подключи интеграцию с неизвестным сервисом, которого сейчас нет.", "DIRECTIVE", epistemics.VerificationNotApplicable)) if path.Kind != work.HelpPathCapabilityGap || !path.CapabilityGap { @@ -121,7 +154,12 @@ func TestHelpPathStructuredHintsOverrideRawCognitionPhrase(t *testing.T) { func planForTest(t *testing.T, message string, act string, verification epistemics.VerificationState) work.PlanningResult { t.Helper() - result, err := BuildPlanningResult(planningInputForTest(message, act, verification), time.Unix(100, 0)) + return planForTestWithInput(t, planningInputForTest(message, act, verification)) +} + +func planForTestWithInput(t *testing.T, input work.PlanningInput) work.PlanningResult { + t.Helper() + result, err := BuildPlanningResult(input, time.Unix(100, 0)) if err != nil { t.Fatal(err) } -- 2.54.0 From 1389d6b24c01f4f85045940fe7ed57a6cc4dfcb9 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 18 Aug 2026 17:47:36 +0300 Subject: [PATCH 10/32] fix: remove stray escaped newline token in planning helper --- modules/work/domain/planning.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/work/domain/planning.go b/modules/work/domain/planning.go index 98e148e..d4d248e 100644 --- a/modules/work/domain/planning.go +++ b/modules/work/domain/planning.go @@ -361,4 +361,4 @@ func isExplicitEmailGapRequest(lower string) bool { func isUnsupportedCapabilityRequest(lower string) bool { return containsAny(lower, "не умеешь", "подключи интеграцию", "подключи сервис", "интеграц", "неизвестн", "нет capability", "создай adapter", "создай адаптер", "сервис, которого сейчас нет") -}\r\n \ No newline at end of file +} -- 2.54.0 From 8448e95257ef7134b9c7217a15875558e9622228 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 21 Aug 2026 01:15:48 +0300 Subject: [PATCH 11/32] fix: preserve selected semantic candidate provenance --- modules/interaction/domain/semantics.go | 11 +++-- modules/interaction/domain/semantics_test.go | 51 ++++++++++++++++++-- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/modules/interaction/domain/semantics.go b/modules/interaction/domain/semantics.go index 84ec283..0531848 100644 --- a/modules/interaction/domain/semantics.go +++ b/modules/interaction/domain/semantics.go @@ -29,6 +29,7 @@ func InterpretUtterance(utterance interaction.HumanUtterance, projects []situati millis := now.UnixMilli() deterministicCandidate := BuildDeterministicCandidate(utterance.Content, utterance.CorrectsUtteranceID, millis) deterministicCandidate.UtteranceID = utterance.ID + deterministicCandidate.SelectionState = interaction.CandidateSelected selectedCandidate := deterministicCandidate result := SemanticResult{ Interpretation: interaction.UtteranceInterpretation{ @@ -103,14 +104,18 @@ func ApplyCognitiveCandidate(result SemanticResult, message string, candidate in matched := false for index := range result.Candidates { - if result.Candidates[index].SelectionState == interaction.CandidateSelected { - result.Candidates[index].SelectionState = interaction.CandidateObserved - } if result.Candidates[index].ID == candidate.ID { result.Candidates[index] = candidate matched = true } } + if selected { + for index := range result.Candidates { + if result.Candidates[index].ID != candidate.ID && result.Candidates[index].SelectionState == interaction.CandidateSelected { + result.Candidates[index].SelectionState = interaction.CandidateObserved + } + } + } if !matched { result.Candidates = append(result.Candidates, candidate) } diff --git a/modules/interaction/domain/semantics_test.go b/modules/interaction/domain/semantics_test.go index ade21f6..810ed62 100644 --- a/modules/interaction/domain/semantics_test.go +++ b/modules/interaction/domain/semantics_test.go @@ -119,10 +119,10 @@ func TestInterpretUtteranceAmbiguousDestructiveRequiresClarification(t *testing. func TestInterpretUtteranceNaturalRussianHelpflowUsesContextAwareCognitionInput(t *testing.T) { cases := []struct { - message string - wantSpeech interaction.SpeechAct - wantCognition bool - allowNoMatch bool + message string + wantSpeech interaction.SpeechAct + wantCognition bool + allowNoMatch bool }{ {message: "расскажи как это работает", wantSpeech: interaction.SpeechActQuestion, wantCognition: true}, {message: "Расскажи, как это работает?", wantSpeech: interaction.SpeechActQuestion, wantCognition: true}, @@ -325,6 +325,49 @@ func TestInterpretUtteranceModalExplanationRequestStillRequestsCognition(t *test } } +func TestInterpretUtteranceSelectsDeterministicCandidateByDefault(t *testing.T) { + result := interpretForTest(t, "основная база называется UPO", nil, "") + if len(result.Candidates) != 1 { + t.Fatalf("expected one deterministic candidate, got %d", len(result.Candidates)) + } + if result.Candidates[0].SelectionState != interaction.CandidateSelected { + t.Fatalf("deterministic candidate must be selected by default, got %s", result.Candidates[0].SelectionState) + } +} + +func TestRejectedCognitiveCandidatePreservesSelectedDeterministicCandidate(t *testing.T) { + result := interpretForTest(t, "основная база называется UPO", nil, "") + candidateID, err := ids.New() + if err != nil { + t.Fatal(err) + } + rejected := interaction.SemanticCandidate{ + ID: interaction.SemanticCandidateID(candidateID), + UtteranceID: result.Interpretation.UtteranceID, + Source: interaction.CandidateSourceCognitive, + PrimarySpeechAct: interaction.SpeechActQuestion, + SecondarySpeechAct: interaction.SpeechActUnknown, + ConfidenceMillis: 700, + Applicability: interaction.SemanticApplicable, + Clarification: interaction.ClarificationNotRequired, + NeedsCognition: false, + SelectionReason: "must not replace a safe assertion with an unrelated candidate", + } + result = ApplyCognitiveCandidate(result, "основная база называется UPO", rejected, nil) + selected := 0 + for _, candidate := range result.Candidates { + if candidate.SelectionState == interaction.CandidateSelected { + selected++ + if candidate.Source != interaction.CandidateSourceDeterministic { + t.Fatalf("rejected cognitive candidate replaced deterministic selection: %#v", candidate) + } + } + } + if selected != 1 { + t.Fatalf("expected exactly one selected candidate after rejection, got %d", selected) + } +} + func interpretForTest(t *testing.T, message string, previous *situation.ActiveProjectFocus, corrects interaction.HumanUtteranceID) SemanticResult { t.Helper() conversation := interaction.Conversation{ID: "conversation-1", ActorID: "actor-1", PrincipalID: "principal-1", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen} -- 2.54.0 From ed67fd7ba904dd52a13b3bf27e3d2dcfa6c7d48d Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 21 Aug 2026 01:19:10 +0300 Subject: [PATCH 12/32] fix: validate cognitive interpretation boundary --- cmd/cognitive-worker/interpretation.go | 27 +++++++++++++++++ cmd/cognitive-worker/interpretation_test.go | 33 +++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 cmd/cognitive-worker/interpretation_test.go diff --git a/cmd/cognitive-worker/interpretation.go b/cmd/cognitive-worker/interpretation.go index 4c01924..c3d814c 100644 --- a/cmd/cognitive-worker/interpretation.go +++ b/cmd/cognitive-worker/interpretation.go @@ -112,6 +112,9 @@ func parseInterpretationCandidate(text string) (cognitive.GatewayInterpretationC if candidate.SecondarySpeechAct != "" && !validSpeechAct(candidate.SecondarySpeechAct) { return cognitive.GatewayInterpretationCandidate{}, false } + if candidate.SecondarySpeechAct == "" { + candidate.SecondarySpeechAct = "UNKNOWN" + } switch candidate.Applicability { case cognitive.InterpretationApplicable, cognitive.InterpretationLowConfidence, cognitive.InterpretationMixedIntent, cognitive.InterpretationNeedsBroader, cognitive.InterpretationClarificationRequired: default: @@ -120,6 +123,11 @@ func parseInterpretationCandidate(text string) (cognitive.GatewayInterpretationC if candidate.Clarification != "NOT_REQUIRED" && candidate.Clarification != "REQUIRED" { return cognitive.GatewayInterpretationCandidate{}, false } + for _, hint := range candidate.CapabilityHints { + if !validCapabilityKind(hint) { + return cognitive.GatewayInterpretationCandidate{}, false + } + } return candidate, true } @@ -131,6 +139,25 @@ func validSpeechAct(value string) bool { return false } +func validCapabilityKind(value capability.CapabilityKind) bool { + switch value { + case capability.CapabilityKindReadEmail, + capability.CapabilityKindSendEmail, + capability.CapabilityKindReadDatabase, + capability.CapabilityKindQueryLogs, + capability.CapabilityKindInspectProject, + capability.CapabilityKindModifyCode, + capability.CapabilityKindRunTests, + capability.CapabilityKindWebResearch, + capability.CapabilityKindCognitiveReasoning, + capability.CapabilityKindScheduleReminder, + capability.CapabilityKindVerifyFact: + return true + default: + return false + } +} + func writeInterpretationResponse(w http.ResponseWriter, response cognitive.GatewayInterpretationResponse) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(response) diff --git a/cmd/cognitive-worker/interpretation_test.go b/cmd/cognitive-worker/interpretation_test.go new file mode 100644 index 0000000..ab36ba8 --- /dev/null +++ b/cmd/cognitive-worker/interpretation_test.go @@ -0,0 +1,33 @@ +package main + +import ( + "testing" + + "git.cin.su/m/Assistant/contracts/capability" +) + +func TestParseInterpretationCandidateNormalizesOmittedSecondarySpeechAct(t *testing.T) { + candidate, ok := parseInterpretationCandidate(`{"primary_speech_act":"QUESTION","confidence_millis":800,"applicability":"APPLICABLE","clarification":"NOT_REQUIRED"}`) + if !ok { + t.Fatal("expected valid candidate") + } + if candidate.SecondarySpeechAct != "UNKNOWN" { + t.Fatalf("expected omitted secondary speech act to normalize to UNKNOWN, got %q", candidate.SecondarySpeechAct) + } +} + +func TestParseInterpretationCandidateRejectsUnknownCapabilityHint(t *testing.T) { + _, ok := parseInterpretationCandidate(`{"primary_speech_act":"QUESTION","confidence_millis":800,"applicability":"APPLICABLE","clarification":"NOT_REQUIRED","capability_hints":["INVENTED_CAPABILITY"]}`) + if ok { + t.Fatal("expected unknown capability hint to be rejected") + } +} + +func TestValidCapabilityKindMatchesContractEnum(t *testing.T) { + if !validCapabilityKind(capability.CapabilityKindCognitiveReasoning) { + t.Fatal("expected cognitive reasoning capability to be valid") + } + if validCapabilityKind(capability.CapabilityKind("INVENTED_CAPABILITY")) { + t.Fatal("invented capability must be invalid") + } +} -- 2.54.0 From 73935b3f94f81a6fc90d079c65a8fb34d5818fc7 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 21 Aug 2026 01:21:00 +0300 Subject: [PATCH 13/32] fix: preserve cognitive deferral in planning --- modules/work/domain/planning.go | 4 ++-- modules/work/domain/planning_test.go | 35 +++++++++++++++++----------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/modules/work/domain/planning.go b/modules/work/domain/planning.go index d4d248e..58719bd 100644 --- a/modules/work/domain/planning.go +++ b/modules/work/domain/planning.go @@ -82,6 +82,7 @@ func DetermineHelpPath(input work.PlanningInput) work.HelpPath { hasQueryLogsHint := hasPlanningHint(input, capability.CapabilityKindQueryLogs) hasVerifyHint := hasPlanningHint(input, capability.CapabilityKindVerifyFact) rawTextFallbackAllowed := len(input.CapabilityHints) == 0 && !input.RequiresClarification && !input.NeedsCognition + unsupportedCapabilityFallbackAllowed := len(input.CapabilityHints) == 0 && !input.RequiresClarification && !input.NeedsCognition if input.SpeechAct != work.SpeechActRef("") && input.SpeechAct != work.SpeechActRef("UNKNOWN") { rawTextFallbackAllowed = false } @@ -100,7 +101,7 @@ func DetermineHelpPath(input work.PlanningInput) work.HelpPath { return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindSendEmail}, CapabilityGap: true, Reason: "email sending needs a communication capability implementation and explicit authority", Reply: "Сейчас у меня ещё не подключена возможность отправлять почту. Я зафиксирую этот capability gap, чтобы позже к нему можно было вернуться."} case hasModifyHint || hasInspectHint || hasQueryLogsHint || hasReadEmailHint || hasRecallHint || hasVerifyHint || needsDurableWorkFromHints(input): return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "semantic hints indicate project/code work even if cognition may still be useful upstream"} - case isUnsupportedCapabilityRequest(lower): + case unsupportedCapabilityFallbackAllowed && isUnsupportedCapabilityRequest(lower): return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{}, CapabilityGap: true, Reason: "requested help needs a currently missing capability and may later trigger acquisition/self-extension", Reply: "Похоже, для этой просьбы нужна capability, которой сейчас нет. Это не задача конкретного executor автоматически: в будущем Домовой сможет найти, подключить или создать подходящую capability и вернуться к исходной потребности."} case rawTextFallbackAllowed && requiresDurableWork(lower): return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "request needs durable coordination, planning, investigation, or outcome tracking"} @@ -144,7 +145,6 @@ func needsCognitiveAnswer(input work.PlanningInput) bool { return containsAny(lower, "модель", "gemma4", "qwen", "спроси", "обратись", "посчитай") } - func ValidatePlanCandidate(candidate work.PlanCandidate) error { if candidate.ID == "" || candidate.GoalID == "" || candidate.Source == "" || candidate.Summary == "" || len(candidate.Steps) == 0 { return ErrInvalidPlanCandidate diff --git a/modules/work/domain/planning_test.go b/modules/work/domain/planning_test.go index 597f569..3370b18 100644 --- a/modules/work/domain/planning_test.go +++ b/modules/work/domain/planning_test.go @@ -109,20 +109,20 @@ func TestHelpPathComplexWorkMaterializesDurablePlanning(t *testing.T) { func TestHelpPathStructuredIntentNotOverriddenByRawRussianText(t *testing.T) { path := DetermineHelpPath(work.PlanningInput{ - ConversationID: "conversation-1", - UtteranceID: "utterance-1", - IntentID: "intent-1", - ActorID: identity.ActorID("actor-1"), - PrincipalID: identity.PrincipalID("principal-1"), - ProjectID: work.ProjectRef("upo"), - ProjectDisplayName: "УПО", - ProjectProvenance: work.ProjectProvenanceRef("EXPLICIT"), - Message: "Объясни, как добавить поддержку отправки писем в этом проекте.", - SpeechAct: work.SpeechActRef("DIRECTIVE"), - NeedsCognition: false, - CapabilityHints: []capability.CapabilityKind{capability.CapabilityKindModifyCode}, + ConversationID: "conversation-1", + UtteranceID: "utterance-1", + IntentID: "intent-1", + ActorID: identity.ActorID("actor-1"), + PrincipalID: identity.PrincipalID("principal-1"), + ProjectID: work.ProjectRef("upo"), + ProjectDisplayName: "УПО", + ProjectProvenance: work.ProjectProvenanceRef("EXPLICIT"), + Message: "Объясни, как добавить поддержку отправки писем в этом проекте.", + SpeechAct: work.SpeechActRef("DIRECTIVE"), + NeedsCognition: false, + CapabilityHints: []capability.CapabilityKind{capability.CapabilityKindModifyCode}, AssertionVerification: epistemics.VerificationNotApplicable, - ReceivedAtUnixMillis: time.Unix(100, 0).UnixMilli(), + ReceivedAtUnixMillis: time.Unix(100, 0).UnixMilli(), }) if path.Kind != work.HelpPathDurableWork || !path.NeedsDurableWork || !path.NeedsDurablePlan { t.Fatalf("structured durable intent should be preserved over raw phrase heuristics: %#v", path) @@ -152,6 +152,15 @@ func TestHelpPathStructuredHintsOverrideRawCognitionPhrase(t *testing.T) { } } +func TestHelpPathDoesNotUseUnsupportedCapabilityHeuristicAfterCognitiveDeferral(t *testing.T) { + input := planningInputForTest("Подключи интеграцию с неизвестным сервисом, которого сейчас нет.", "DIRECTIVE", epistemics.VerificationNotApplicable) + input.NeedsCognition = true + path := DetermineHelpPath(input) + if path.Kind == work.HelpPathCapabilityGap { + t.Fatalf("raw unsupported-capability phrase must not override deferred cognitive interpretation: %#v", path) + } +} + func planForTest(t *testing.T, message string, act string, verification epistemics.VerificationState) work.PlanningResult { t.Helper() return planForTestWithInput(t, planningInputForTest(message, act, verification)) -- 2.54.0 From 293cb4f4c1cf9f392d83d06812c7a26de17102ec Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 21 Aug 2026 01:23:11 +0300 Subject: [PATCH 14/32] fix: avoid exposing client error details --- cmd/creator-web/static/assets/chat.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cmd/creator-web/static/assets/chat.js b/cmd/creator-web/static/assets/chat.js index 34e4c95..dede0cc 100644 --- a/cmd/creator-web/static/assets/chat.js +++ b/cmd/creator-web/static/assets/chat.js @@ -189,10 +189,8 @@ async function sendChatMessage(text, options = {}) { await loadToday(); activateTab("today-panel"); } - } catch (error) { - const raw = String(error && error.message ? error.message : "").trim(); - const detail = raw ? `: ${raw}` : ""; - appendMessage("assistant", `Не могу отправить сообщение${detail}. Попробуйте ещё раз.`); + } catch { + appendMessage("assistant", "Не могу отправить сообщение. Попробуйте ещё раз."); } finally { sendButton.disabled = false; messageInput.focus(); -- 2.54.0 From d800e49a2a1d8e1d7e031469543b32c3f1744cc2 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 21 Aug 2026 01:26:42 +0300 Subject: [PATCH 15/32] fix: sanitize creator console errors --- cmd/creator-web/static/assets/console.js | 40 +++++++++++++++--------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/cmd/creator-web/static/assets/console.js b/cmd/creator-web/static/assets/console.js index 9ccec82..c4e32f5 100644 --- a/cmd/creator-web/static/assets/console.js +++ b/cmd/creator-web/static/assets/console.js @@ -39,6 +39,18 @@ function notify(message) { toastTimer = setTimeout(() => { toast.hidden = true; }, 4200); } +function safeClientError(error) { + const message = error && typeof error.message === "string" ? error.message : ""; + switch (message) { + case "creator authentication denied": + return "Логин или пароль не принят."; + case "Сессия отклонена": + return "Сессия истекла. Войдите снова."; + default: + return "Операция недоступна. Попробуйте ещё раз."; + } +} + async function request(path, options = {}) { const headers = new Headers(options.headers || {}); if (sessionToken) headers.set("Authorization", `Bearer ${sessionToken}`); @@ -128,7 +140,7 @@ loginForm.addEventListener("submit", async (event) => { byId("creator-password").value = ""; await showConsole(); } catch (error) { - loginError.textContent = error.message === "creator authentication denied" ? "Логин или пароль не принят." : error.message; + loginError.textContent = safeClientError(error); } finally { button.disabled = false; button.firstElementChild.textContent = "Открыть консоль"; @@ -185,7 +197,7 @@ advisoryForm.addEventListener("submit", async (event) => { byId("advisory-result").hidden = false; } catch (error) { setText("advisory-state", "ОШИБКА"); - setText("advisory-text", error.message); + setText("advisory-text", safeClientError(error)); byId("advisory-result").hidden = false; } finally { byId("advisory-egress-ack").checked = false; @@ -272,7 +284,7 @@ async function loadLearning() { list.replaceChildren(); const failed = document.createElement("p"); failed.className = "register-empty activity-error"; - failed.textContent = error.message; + failed.textContent = safeClientError(error); list.append(failed); }); setText("learning-count", "ОШИБКА"); @@ -371,7 +383,7 @@ async function loadActionHistory() { list.replaceChildren(); const failed = document.createElement("p"); failed.className = "register-empty activity-error"; - failed.textContent = error.message; + failed.textContent = safeClientError(error); list.append(failed); setText("action-count", "ОШИБКА"); } finally { @@ -430,7 +442,7 @@ async function reconcileHistoryAction(actionID, button) { await loadActionHistory(); if (currentProspective) await loadProspectiveRegister(currentProspective.ID); } catch (error) { - notify(error.message); + notify(safeClientError(error)); button.disabled = false; button.textContent = "Сверить"; } @@ -450,7 +462,7 @@ async function loadFocus(query) { } catch (error) { setSignal("focus", "ошибка", "error"); setBadge("focus", "НЕДОСТУПЕН", "bad"); - setText("focus-message", error.message); + setText("focus-message", safeClientError(error)); byId("connection-state").classList.add("offline"); } } @@ -472,7 +484,7 @@ async function loadTask(taskID, query) { currentTask = null; setSignal("task", "отклонена", "error"); setBadge("task", "ВНЕ ФОКУСА", "bad"); - setText("task-message", error.message); + setText("task-message", safeClientError(error)); updateActionReadiness(); } } @@ -511,12 +523,12 @@ async function loadProspectiveRegister(selectedID) { list.replaceChildren(); const failed = document.createElement("p"); failed.className = "register-empty"; - failed.textContent = error.message; + failed.textContent = safeClientError(error); list.append(failed); setText("prospective-count", "ОШИБКА"); setSignal("prospective", "ошибка", "error"); setBadge("prospective", "НЕДОСТУПНА", "bad"); - setText("prospective-message", error.message); + setText("prospective-message", safeClientError(error)); } } @@ -572,7 +584,7 @@ async function loadProspective(prospectiveID) { setSignal("prospective", "ошибка", "error"); setBadge("prospective", "НЕДОСТУПНА", "bad"); setText("prospective-state", "UNKNOWN"); - setText("prospective-message", error.message); + setText("prospective-message", safeClientError(error)); updateActionReadiness(); } } @@ -594,7 +606,7 @@ async function loadCapability(capabilityID) { setSignal("capability", "ошибка", "error"); setBadge("capability", "НЕДОСТУПНА", "bad"); setText("capability-state", "UNKNOWN"); - setText("capability-reason", error.message); + setText("capability-reason", safeClientError(error)); } } @@ -700,7 +712,7 @@ byId("confirm-notification").addEventListener("click", async () => { await loadActionHistory(); notify(result.State === "SUCCEEDED" ? "Receiver принял уведомление." : "Исход отправки требует сверки."); } catch (error) { - setText("notification-confirmation-error", error.message); + setText("notification-confirmation-error", safeClientError(error)); } finally { button.disabled = false; button.textContent = "Подтверждаю отправку"; @@ -730,7 +742,7 @@ byId("confirm-satisfaction").addEventListener("click", async () => { notify(result.State === "SUCCEEDED" ? "Исполнение зафиксировано через Commit." : "Исход требует проверки."); await loadProspectiveRegister(command.ProspectiveItemID); } catch (error) { - setText("confirmation-error", error.message); + setText("confirmation-error", safeClientError(error)); } finally { button.disabled = false; button.textContent = "Подтверждаю переход"; @@ -764,7 +776,7 @@ byId("reconcile-action").addEventListener("click", async () => { notify(result.State === "SUCCEEDED" ? "Исход подтверждён." : "Исход пока неизвестен."); if (currentProspective) await loadProspectiveRegister(currentProspective.ID); } catch (error) { - notify(error.message); + notify(safeClientError(error)); } finally { button.disabled = false; button.textContent = "Проверить исход"; -- 2.54.0 From 4f53c110ea900d103a5f7e43c747de73536c9c72 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 21 Aug 2026 01:27:29 +0300 Subject: [PATCH 16/32] docs: record current local repair checkpoint --- docs/PROJECT_STATUS.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 5a50ab6..b6fa110 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -2,6 +2,15 @@ Audit date: 2026-08-17. Active implementation: DEV-000072 revision 1. +## Post-audit local repair checkpoint — 2026-08-21 + +- `CURRENT_BRANCH = codex/domovoy-understanding-repair-v1` +- `CURRENT_HEAD = d800e49` (5 local repair commits ahead of `origin/codex/domovoy-understanding-repair-v1`) +- `LOCAL_VALIDATION = PASS: go test ./...; go vet ./...; devctl preflight; check-diff; check-architecture; git diff --check; node --check for creator-web chat.js and console.js` +- `REPAIRS = selected semantic candidate preservation; cognitive gateway candidate validation; cognitive deferral planning precedence; safe creator-web and creator-console error rendering` +- `PRODUCTION_STATUS = not verified for CURRENT_HEAD; no promotion, migration application or live /chat acceptance is claimed here` +- `NEXT_REQUIRED_EVIDENCE = promote the exact evaluated artifact, apply migration 027 if absent, then verify production health and authenticated /chat acceptance` + ## DEV-000072 checkpoint - `DEV_000072_STATUS = LOCAL_ACCEPTANCE_PASS` -- 2.54.0 From 1ceb5f6761efafe6bd0cb3967358706c3a3b080f Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 21 Aug 2026 01:29:07 +0300 Subject: [PATCH 17/32] docs: record production runtime blockers --- docs/PROJECT_STATUS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index b6fa110..3eae54e 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -10,6 +10,9 @@ Audit date: 2026-08-17. Active implementation: DEV-000072 revision 1. - `REPAIRS = selected semantic candidate preservation; cognitive gateway candidate validation; cognitive deferral planning precedence; safe creator-web and creator-console error rendering` - `PRODUCTION_STATUS = not verified for CURRENT_HEAD; no promotion, migration application or live /chat acceptance is claimed here` - `NEXT_REQUIRED_EVIDENCE = promote the exact evaluated artifact, apply migration 027 if absent, then verify production health and authenticated /chat acceptance` +- `CURRENT_PRODUCTION_READONLY_AUDIT = 2026-08-21: https://assistant.cin.su/healthz returned HTTP 502; assistant-postgres-1 is unhealthy; assistant-continuity-runtime-1 is restarting; core-runtime, commit-runtime and creator-web are not running` +- `CURRENT_PRODUCTION_BLOCKERS = deployed continuity runtime has empty COMMIT_EXECUTOR_TOKEN; deployed PostgreSQL healthcheck expands to pg_isready -U -d assistant because POSTGRES_SUPERUSER is empty` +- `PRODUCTION_BLOCKER_SCOPE = deployment/credential configuration outside DEV-000072 allowed paths; no secret, restart, migration or deployment mutation was performed` ## DEV-000072 checkpoint -- 2.54.0 From da94ed17a68f22141a02c9bb43a241ec612ac9d8 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 21 Aug 2026 01:30:13 +0300 Subject: [PATCH 18/32] docs: record production artifact and schema evidence --- docs/PROJECT_STATUS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 3eae54e..1cec187 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -13,6 +13,8 @@ Audit date: 2026-08-17. Active implementation: DEV-000072 revision 1. - `CURRENT_PRODUCTION_READONLY_AUDIT = 2026-08-21: https://assistant.cin.su/healthz returned HTTP 502; assistant-postgres-1 is unhealthy; assistant-continuity-runtime-1 is restarting; core-runtime, commit-runtime and creator-web are not running` - `CURRENT_PRODUCTION_BLOCKERS = deployed continuity runtime has empty COMMIT_EXECUTOR_TOKEN; deployed PostgreSQL healthcheck expands to pg_isready -U -d assistant because POSTGRES_SUPERUSER is empty` - `PRODUCTION_BLOCKER_SCOPE = deployment/credential configuration outside DEV-000072 allowed paths; no secret, restart, migration or deployment mutation was performed` +- `CURRENT_PRODUCTION_ARTIFACT = revision 2e0c0236aa29d493d1ee9579354c244029cea09a on all inspected project-built containers; CURRENT_HEAD is not promoted` +- `CURRENT_PRODUCTION_SCHEMA = migration 027 is present: interaction.semantic_candidates, interaction.semantic_candidate_capability_hints, and interpretation columns secondary_speech_act/applicability/selection_reason were confirmed read-only` ## DEV-000072 checkpoint -- 2.54.0 From 49036e6397911c107daeec5d4d435d3e5c45f1ea Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 21 Aug 2026 01:42:24 +0300 Subject: [PATCH 19/32] fix: fail closed on missing production inputs --- deploy/docker-compose.prod.yml | 10 +- dev/ACTIVE_TASK.json | 4 +- ...000073-production-compose-fail-closed.json | 96 +++++++++++++++++++ 3 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 dev/tasks/DEV-000073-production-compose-fail-closed.json diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml index c90aef1..794be2d 100644 --- a/deploy/docker-compose.prod.yml +++ b/deploy/docker-compose.prod.yml @@ -8,8 +8,8 @@ services: restart: unless-stopped environment: POSTGRES_DB: ${POSTGRES_DB:-assistant} - POSTGRES_USER: ${POSTGRES_SUPERUSER} - POSTGRES_PASSWORD: ${POSTGRES_SUPERUSER_PASSWORD} + POSTGRES_USER: ${POSTGRES_SUPERUSER:?POSTGRES_SUPERUSER is required} + POSTGRES_PASSWORD: ${POSTGRES_SUPERUSER_PASSWORD:?POSTGRES_SUPERUSER_PASSWORD is required} CONTINUITY_DB_PASSWORD: ${CONTINUITY_DB_PASSWORD} IDENTITY_DB_PASSWORD: ${IDENTITY_DB_PASSWORD} AUTHORITY_DB_PASSWORD: ${AUTHORITY_DB_PASSWORD} @@ -23,7 +23,7 @@ services: - assistant_pg:/var/lib/postgresql - assistant_core_secrets:/run/secrets:ro healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_SUPERUSER} -d ${POSTGRES_DB:-assistant}"] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_SUPERUSER:?POSTGRES_SUPERUSER is required} -d ${POSTGRES_DB:-assistant}"] interval: 10s timeout: 5s retries: 20 @@ -95,7 +95,7 @@ services: POSTGRES_USER: continuity_role POSTGRES_PASSWORD: ${CONTINUITY_DB_PASSWORD} CORE_RUNTIME_URL: http://core-runtime:8080 - COMMIT_EXECUTOR_TOKEN: ${COMMIT_EXECUTOR_TOKEN} + COMMIT_EXECUTOR_TOKEN: ${COMMIT_EXECUTOR_TOKEN:?COMMIT_EXECUTOR_TOKEN is required} commit-runtime: build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: commit-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } } @@ -111,7 +111,7 @@ services: AUTHORITY_POSTGRES_PASSWORD: ${AUTHORITY_DB_PASSWORD} CORE_RUNTIME_URL: http://core-runtime:8080 CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081 - COMMIT_EXECUTOR_TOKEN: ${COMMIT_EXECUTOR_TOKEN} + COMMIT_EXECUTOR_TOKEN: ${COMMIT_EXECUTOR_TOKEN:?COMMIT_EXECUTOR_TOKEN is required} NOTIFICATION_WRITE_TOKEN_FILE: /run/notification-writer/token NOTIFICATION_WRITE_SECRET_REF: domovoy-notification-write-v1 # External mutation credentials belong here only via Portainer/Docker secrets/env injection. diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 3052249..268f982 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000072-semantic-interpretation-deferral-boundary.json", - "task_id": "DEV-000072", + "task_file": "dev/tasks/DEV-000073-production-compose-fail-closed.json", + "task_id": "DEV-000073", "task_revision": 1 } diff --git a/dev/tasks/DEV-000073-production-compose-fail-closed.json b/dev/tasks/DEV-000073-production-compose-fail-closed.json new file mode 100644 index 0000000..23a69bf --- /dev/null +++ b/dev/tasks/DEV-000073-production-compose-fail-closed.json @@ -0,0 +1,96 @@ +{ + "task_id": "DEV-000073", + "task_revision": 1, + "architecture_revision": "DEV-000072-baseline", + "base_repo_revision": "da94ed1", + "risk_tier": "E", + "intent": "Make Assistant production Compose fail closed when required PostgreSQL and executor inputs are absent, without mutating neighboring projects or storing secrets in the repository.", + "requirement_ids": [ + "Production missing inputs must fail before container startup", + "Assistant-only deployment scope", + "No secret values in Git", + "No neighboring project mutation" + ], + "allowed_change_scope": { + "modules": [], + "paths": [ + "deploy/docker-compose.prod.yml", + "docs/PROJECT_STATUS.md", + "dev/ACTIVE_TASK.json", + "dev/tasks/DEV-000073-production-compose-fail-closed.json" + ] + }, + "forbidden_change_scope": { + "paths": [ + "architecture/**", + "security/**", + "modules/**", + "contracts/**", + "database/migrations/**", + "deploy/docker-compose.dev.yml", + ".gitea/**" + ] + }, + "allowed_architecture_delta": { + "new_cross_module_dependencies": 0, + "new_canonical_owners": 0, + "new_privileges": 0, + "new_third_party_dependencies": 0, + "new_provider_adapters": 0, + "new_database_owner_roles": 0 + }, + "contracts": { + "consume": [ + "existing production Compose topology", + "existing external secret injection" + ], + "may_change": [] + }, + "semantic_owner": "none-production-configuration", + "canonical_state": [], + "state_transitions": [ + "missing required production input -> Compose validation failure", + "all required inputs present -> existing Assistant stack topology remains unchanged" + ], + "side_effects_and_privileges": [ + "No secret creation or storage in Git", + "No deployment of neighboring projects", + "Promotion only after exact artifact and health checks" + ], + "persistence_and_migrations": [ + "No database migration", + "No canonical state change" + ], + "failure_retry_restart": [ + "Missing required input fails before Compose startup", + "Existing Assistant containers remain untouched until explicit promotion" + ], + "assumptions": [ + "Production control plane is docker-prod / docker.cin.su", + "Required secret values remain external to the repository", + "Neighboring Docker projects must not be included in any Compose operation" + ], + "acceptance_scenarios": [ + "Compose rejects missing POSTGRES_SUPERUSER", + "Compose rejects missing POSTGRES_SUPERUSER_PASSWORD", + "Compose rejects missing COMMIT_EXECUTOR_TOKEN", + "Compose renders with synthetic non-secret values", + "scope check passes", + "Assistant-only production inspection identifies exact artifact and health state" + ], + "required_tests": [ + "docker compose config -q with missing inputs", + "docker compose config -q with synthetic inputs", + "go test ./...", + "go vet ./...", + "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", + "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", + "go run ./cmd/devctl check-architecture", + "production Assistant-only health and artifact inspection" + ], + "terminal_gap_states": [ + "BLOCKED_BY_MISSING_CAPABILITY", + "BLOCKED_BY_CONFLICT", + "REQUIRES_MIGRATION_DECISION" + ] +} -- 2.54.0 From 3aa0a1d452449222f3e3231ec2ac62093ce80d2d Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 21 Aug 2026 01:43:06 +0300 Subject: [PATCH 20/32] docs: record production promotion access gap --- docs/PROJECT_STATUS.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 1cec187..c8a4d2d 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -5,11 +5,13 @@ Audit date: 2026-08-17. Active implementation: DEV-000072 revision 1. ## Post-audit local repair checkpoint — 2026-08-21 - `CURRENT_BRANCH = codex/domovoy-understanding-repair-v1` -- `CURRENT_HEAD = d800e49` (5 local repair commits ahead of `origin/codex/domovoy-understanding-repair-v1`) -- `LOCAL_VALIDATION = PASS: go test ./...; go vet ./...; devctl preflight; check-diff; check-architecture; git diff --check; node --check for creator-web chat.js and console.js` -- `REPAIRS = selected semantic candidate preservation; cognitive gateway candidate validation; cognitive deferral planning precedence; safe creator-web and creator-console error rendering` +- `CURRENT_HEAD = 49036e6` (9 local repair/config commits ahead of `origin/codex/domovoy-understanding-repair-v1`) +- `ACTIVE_TASK = DEV-000073 production Compose fail-closed configuration` +- `LOCAL_VALIDATION = PASS: go test ./...; go vet ./...; devctl preflight; check-diff; check-architecture; git diff --check; node --check for creator-web chat.js and console.js; docker compose config with missing-input rejection and synthetic-input render` +- `REPAIRS = selected semantic candidate preservation; cognitive gateway candidate validation; cognitive deferral planning precedence; safe creator-web and creator-console error rendering; fail-closed production Compose inputs` - `PRODUCTION_STATUS = not verified for CURRENT_HEAD; no promotion, migration application or live /chat acceptance is claimed here` - `NEXT_REQUIRED_EVIDENCE = promote the exact evaluated artifact, apply migration 027 if absent, then verify production health and authenticated /chat acceptance` +- `PROMOTION_ACCESS = blocked: PORTAINER_API_KEY and PORTAINER_ENDPOINT_ID are not present in the current environment; production stack was not mutated` - `CURRENT_PRODUCTION_READONLY_AUDIT = 2026-08-21: https://assistant.cin.su/healthz returned HTTP 502; assistant-postgres-1 is unhealthy; assistant-continuity-runtime-1 is restarting; core-runtime, commit-runtime and creator-web are not running` - `CURRENT_PRODUCTION_BLOCKERS = deployed continuity runtime has empty COMMIT_EXECUTOR_TOKEN; deployed PostgreSQL healthcheck expands to pg_isready -U -d assistant because POSTGRES_SUPERUSER is empty` - `PRODUCTION_BLOCKER_SCOPE = deployment/credential configuration outside DEV-000072 allowed paths; no secret, restart, migration or deployment mutation was performed` -- 2.54.0 From 407275c796e90f1ceace17b7a2826d06c196c99b Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 21 Aug 2026 02:23:40 +0300 Subject: [PATCH 21/32] docs: record Assistant production recovery --- docs/PROJECT_STATUS.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index c8a4d2d..6810e12 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -2,19 +2,27 @@ Audit date: 2026-08-17. Active implementation: DEV-000072 revision 1. +## Production recovery checkpoint — 2026-08-21 + +- `RECOVERY_ACTION = assistant-only docker compose up -d --no-build using ignored local secrets/docker-prod-bootstrap.env; no build, prune, volume deletion or neighbor-stack command` +- `RECOVERY_RESULT = PASS: assistant-creator-web healthy on host port 18080; assistant-postgres healthy; core-runtime, continuity-runtime, commit-runtime and cognitive-worker running; https://assistant.cin.su/healthz returned HTTP 200` +- `ROUTING_FINDING = Caddy already routes assistant.cin.su to host.docker.internal:18080; the 502 was caused by creator-web being Created with no listener, not by a bad Caddy port` +- `PRODUCTION_ARTIFACT_AFTER_RECOVERY = revision 2e0c0236aa29d493d1ee9579354c244029cea09a on inspected Assistant containers; CURRENT_HEAD remains local and is not promoted` +- `NEIGHBOR_SCOPE = read-only before/after snapshots showed atlas, llm-local, adapter-1c, gitea, portainer and Caddy still running; no neighbor container was targeted` + ## Post-audit local repair checkpoint — 2026-08-21 - `CURRENT_BRANCH = codex/domovoy-understanding-repair-v1` -- `CURRENT_HEAD = 49036e6` (9 local repair/config commits ahead of `origin/codex/domovoy-understanding-repair-v1`) +- `CURRENT_HEAD = 3aa0a1d` (10 local repair/config commits ahead of `origin/codex/domovoy-understanding-repair-v1`) - `ACTIVE_TASK = DEV-000073 production Compose fail-closed configuration` - `LOCAL_VALIDATION = PASS: go test ./...; go vet ./...; devctl preflight; check-diff; check-architecture; git diff --check; node --check for creator-web chat.js and console.js; docker compose config with missing-input rejection and synthetic-input render` - `REPAIRS = selected semantic candidate preservation; cognitive gateway candidate validation; cognitive deferral planning precedence; safe creator-web and creator-console error rendering; fail-closed production Compose inputs` -- `PRODUCTION_STATUS = not verified for CURRENT_HEAD; no promotion, migration application or live /chat acceptance is claimed here` +- `PRODUCTION_STATUS = recovered for existing production artifact; not verified for CURRENT_HEAD; no current-branch promotion, migration application or live /chat acceptance is claimed here` - `NEXT_REQUIRED_EVIDENCE = promote the exact evaluated artifact, apply migration 027 if absent, then verify production health and authenticated /chat acceptance` -- `PROMOTION_ACCESS = blocked: PORTAINER_API_KEY and PORTAINER_ENDPOINT_ID are not present in the current environment; production stack was not mutated` -- `CURRENT_PRODUCTION_READONLY_AUDIT = 2026-08-21: https://assistant.cin.su/healthz returned HTTP 502; assistant-postgres-1 is unhealthy; assistant-continuity-runtime-1 is restarting; core-runtime, commit-runtime and creator-web are not running` -- `CURRENT_PRODUCTION_BLOCKERS = deployed continuity runtime has empty COMMIT_EXECUTOR_TOKEN; deployed PostgreSQL healthcheck expands to pg_isready -U -d assistant because POSTGRES_SUPERUSER is empty` -- `PRODUCTION_BLOCKER_SCOPE = deployment/credential configuration outside DEV-000072 allowed paths; no secret, restart, migration or deployment mutation was performed` +- `PROMOTION_ACCESS = Portainer API key remains unavailable; recovery used the authorized Docker production context and ignored local secret file without printing or copying its values` +- `CURRENT_PRODUCTION_READONLY_AUDIT = 2026-08-21 after recovery: https://assistant.cin.su/healthz returned HTTP 200; assistant-postgres-1 and assistant-creator-web-1 healthy; all Assistant runtime containers running` +- `CURRENT_PRODUCTION_BLOCKERS = current branch 3aa0a1d is not the deployed artifact; authenticated /chat acceptance remains pending` +- `PRODUCTION_BLOCKER_SCOPE = exact current-branch promotion and live acceptance remain outstanding; no neighbor stack was mutated` - `CURRENT_PRODUCTION_ARTIFACT = revision 2e0c0236aa29d493d1ee9579354c244029cea09a on all inspected project-built containers; CURRENT_HEAD is not promoted` - `CURRENT_PRODUCTION_SCHEMA = migration 027 is present: interaction.semantic_candidates, interaction.semantic_candidate_capability_hints, and interpretation columns secondary_speech_act/applicability/selection_reason were confirmed read-only` -- 2.54.0 From 3ed64631f6a0defbf90c7981179f99d26de34ca9 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 21 Aug 2026 02:48:45 +0300 Subject: [PATCH 22/32] feat: make Creator cabinet operable for humans --- cmd/creator-web/static/assets/console.css | 60 ++++++++++ cmd/creator-web/static/assets/console.js | 106 ++++++++++++++++++ cmd/creator-web/static/index.html | 43 ++++++- dev/ACTIVE_TASK.json | 4 +- ...DEV-000074-creator-cabinet-operations.json | 92 +++++++++++++++ 5 files changed, 301 insertions(+), 4 deletions(-) create mode 100644 dev/tasks/DEV-000074-creator-cabinet-operations.json diff --git a/cmd/creator-web/static/assets/console.css b/cmd/creator-web/static/assets/console.css index 8dbd237..6fe659a 100644 --- a/cmd/creator-web/static/assets/console.css +++ b/cmd/creator-web/static/assets/console.css @@ -63,6 +63,9 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px .wordmark strong { font: 400 20px/1 var(--serif); } .wordmark small { margin-top: 4px; color: var(--muted); font: 9px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; } .topbar-actions { display: flex; align-items: center; gap: 10px; } +.topbar-nav { display: flex; align-items: center; gap: 4px; margin-right: 8px; } +.topbar-nav a { padding: 9px 10px; color: var(--muted); text-decoration: none; font: 600 9px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; } +.topbar-nav a:hover, .topbar-nav a:focus-visible { color: var(--ink); background: var(--panel-high); outline: 0; } .connection { margin-right: 12px; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; } .connection i { width: 7px; height: 7px; display: inline-block; margin-right: 8px; border-radius: 50%; background: var(--good); box-shadow: 0 0 14px rgba(120, 198, 163, .6); } .connection.offline i { background: var(--bad); box-shadow: none; } @@ -77,6 +80,12 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px .context-form label:first-child { margin-top: 0; } .context-form input { height: 40px; padding: 0 10px; font: 11px/1 var(--mono); } .secondary-button { height: 42px; margin-top: 20px; color: #11150f; background: var(--signal); font-size: 12px; } +.section-nav { display: grid; gap: 3px; margin-top: 4px; } +.section-nav-link { display: grid; grid-template-columns: 28px 1fr; gap: 2px 10px; padding: 11px 10px; border-left: 2px solid transparent; color: var(--muted); text-decoration: none; transition: background .18s, border-color .18s, color .18s; } +.section-nav-link:hover, .section-nav-link:focus-visible, .section-nav-link.active { border-left-color: var(--signal); color: var(--ink); background: rgba(201,168,95,.07); outline: 0; } +.section-nav-link span { grid-row: 1 / span 2; color: var(--signal); font: 9px/1.4 var(--mono); } +.section-nav-link b { font: 600 11px/1.2 var(--sans); } +.section-nav-link small { color: var(--faint); font: 9px/1.2 var(--mono); } .session-stamp { margin-top: auto; padding-top: 20px; display: grid; gap: 6px; border-top: 1px solid var(--line); } .session-stamp span { margin-top: 8px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .14em; } .session-stamp strong { overflow: hidden; color: var(--muted); font: 10px/1.4 var(--mono); text-overflow: ellipsis; } @@ -94,6 +103,46 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px .signal-strip strong[data-state="error"] { color: var(--bad); } .signal-strip strong[data-state="loading"] { color: var(--warn); animation: pulse 1.1s ease-in-out infinite; } +.diagnostics-panel, .settings-panel { margin-bottom: 18px; border: 1px solid var(--line); background: rgba(16,23,20,.82); } +.diagnostics-panel { border-color: rgba(120,198,163,.32); box-shadow: inset 3px 0 0 rgba(120,198,163,.45); } +.panel-heading { min-height: 68px; padding: 14px 18px; display: flex; align-items: center; justify-content: space-between; gap: 18px; border-bottom: 1px solid var(--line); } +.panel-heading p { margin: 0 0 6px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; text-transform: uppercase; } +.panel-heading h2 { margin: 0; font: 400 23px/1 var(--serif); } +.panel-heading-tools { display: flex; align-items: center; gap: 14px; } +.panel-heading-tools > span, .settings-saved { color: var(--muted); font: 9px/1.3 var(--mono); letter-spacing: .06em; text-transform: uppercase; } +.compact-button { width: auto; min-width: 142px; height: 38px; margin: 0; padding: 0 13px; } +.diagnostic-intro { padding: 13px 18px; display: flex; align-items: center; gap: 10px; border-bottom: 1px solid var(--line-soft); color: var(--muted); } +.diagnostic-intro p { margin: 0; font-size: 11px; line-height: 1.45; } +.diagnostic-pulse { width: 7px; height: 7px; flex: 0 0 auto; border-radius: 50%; background: var(--good); box-shadow: 0 0 12px rgba(120,198,163,.7); } +.diagnostic-list { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); } +.diagnostic-list[aria-busy="true"] { opacity: .7; } +.diagnostic-item { min-width: 0; min-height: 92px; padding: 14px 16px; display: grid; grid-template-columns: 9px 1fr auto; gap: 9px; align-items: start; border-right: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); } +.diagnostic-item:nth-child(4n) { border-right: 0; } +.diagnostic-item > i { width: 7px; height: 7px; margin-top: 4px; border-radius: 50%; background: var(--faint); } +.diagnostic-item[data-state="ok"] > i { background: var(--good); box-shadow: 0 0 9px rgba(120,198,163,.6); } +.diagnostic-item[data-state="error"] > i { background: var(--bad); } +.diagnostic-item[data-state="loading"] > i { background: var(--warn); animation: pulse 1.1s ease-in-out infinite; } +.diagnostic-item strong { display: block; color: var(--ink); font: 600 10px/1.3 var(--mono); } +.diagnostic-item span { display: block; margin-top: 6px; color: var(--muted); font-size: 10px; line-height: 1.35; } +.diagnostic-item em { color: var(--faint); font: normal 9px/1.2 var(--mono); text-transform: uppercase; } +.diagnostic-item[data-state="ok"] em { color: var(--good); } +.diagnostic-item[data-state="error"] em { color: var(--bad); } +.settings-grid { padding: 18px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 20px; } +.setting-field, .setting-check { min-width: 0; display: grid; gap: 7px; color: var(--ink); } +.setting-field > span, .setting-check b { font: 600 10px/1.2 var(--mono); letter-spacing: .08em; text-transform: uppercase; } +.setting-field small, .setting-check small { color: var(--muted); font-size: 11px; line-height: 1.4; } +.setting-field select { width: 100%; min-height: 40px; padding: 0 10px; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: #0b110e; font: 11px/1 var(--mono); } +.setting-check { grid-template-columns: 18px 1fr; align-items: start; gap: 10px; padding: 10px 12px; border: 1px solid var(--line-soft); } +.setting-check input { width: 16px; height: 16px; margin: 2px 0 0; accent-color: var(--signal); } +.setting-check span { display: grid; gap: 7px; } +.settings-actions { display: flex; align-items: center; justify-content: flex-end; gap: 14px; grid-column: 1 / -1; padding-top: 2px; } +.settings-actions .text-button { margin: 0; } +html[data-density="compact"] .data-card { padding: 18px; } +html[data-density="compact"] .data-card dl div { padding-block: 10px; } +html[data-density="compact"] .activity-row { min-height: 58px; padding-block: 8px; } +html[data-density="compact"] .learning-row { min-height: 54px; padding-block: 9px; } +html[data-reduced-motion="true"] *, html[data-reduced-motion="true"] *::before, html[data-reduced-motion="true"] *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; transition-duration: .01ms !important; } + .advisor-panel { margin-bottom: 18px; padding: 24px; display: grid; grid-template-columns: minmax(220px, .72fr) minmax(320px, 1.28fr); gap: 18px 34px; border: 1px solid rgba(201,168,95,.42); background: linear-gradient(125deg, rgba(201,168,95,.1), rgba(16,23,20,.88) 44%, rgba(12,20,17,.96)); box-shadow: inset 3px 0 0 rgba(201,168,95,.58); } .advisor-intro { align-self: center; } .advisor-intro > p { margin: 0 0 10px; color: var(--signal); font: 600 9px/1 var(--mono); letter-spacing: .15em; } @@ -278,6 +327,9 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px .advisor-panel { grid-template-columns: 1fr; } .capability-card { grid-column: auto; } .prospective-card { grid-column: auto; } + .diagnostic-list { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .diagnostic-item:nth-child(4n) { border-right: 1px solid var(--line-soft); } + .diagnostic-item:nth-child(2n) { border-right: 0; } } @media (max-width: 680px) { @@ -285,6 +337,7 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px .console { display: block; } .topbar { height: 66px; padding: 0 14px; } .wordmark small, .connection, .text-button { display: none; } + .topbar-nav { display: none; } .context-rail { padding: 24px 18px; border-right: 0; border-bottom: 1px solid var(--line); } .rail-copy { max-width: 48ch; } .session-stamp { margin-top: 8px; grid-template-columns: auto 1fr; align-items: center; } @@ -293,6 +346,13 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px .workspace-head { align-items: start; flex-direction: column; } .last-update { text-align: left; } .signal-strip { grid-template-columns: 1fr; } + .panel-heading { align-items: flex-start; flex-direction: column; } + .panel-heading-tools { width: 100%; justify-content: space-between; } + .diagnostic-list { grid-template-columns: 1fr; } + .diagnostic-item, .diagnostic-item:nth-child(2n), .diagnostic-item:nth-child(4n) { border-right: 0; } + .settings-grid { grid-template-columns: 1fr; padding: 16px; } + .settings-actions { grid-column: auto; align-items: stretch; flex-direction: column; } + .settings-actions .primary-button { width: 100%; } .signal-strip div { border-right: 0; border-bottom: 1px solid var(--line); } .advisor-panel { padding: 20px 16px; gap: 20px; } .advisor-controls { align-items: stretch; flex-direction: column; } diff --git a/cmd/creator-web/static/assets/console.js b/cmd/creator-web/static/assets/console.js index c4e32f5..3eb6830 100644 --- a/cmd/creator-web/static/assets/console.js +++ b/cmd/creator-web/static/assets/console.js @@ -7,14 +7,17 @@ const advisoryForm = byId("advisory-form"); const loginError = byId("login-error"); const toast = byId("toast"); const contextFields = ["session-id", "workspace-id", "task-id", "prospective-id", "capability-id"]; +const settingsStorageKey = "domovoy.creator.settings"; let sessionToken = sessionStorage.getItem("domovoy.creator.session") || ""; let toastTimer; +let refreshTimer; let currentTask = null; let currentProspective = null; let pendingCommand = null; let pendingNotification = null; let lastAuthorizedActionID = ""; let advisoryPolicy = {Mode: "deterministic", ExternalEgress: false, AcknowledgementRequired: false}; +let uiSettings = {density: "comfortable", refresh: "off", motion: false}; function setText(id, value, fallback = "—") { byId(id).textContent = value === undefined || value === null || value === "" ? fallback : String(value); @@ -92,6 +95,7 @@ async function showConsole() { setText("session-expiry", expiry); loginView.hidden = true; consoleView.hidden = false; + restoreSettings(); restoreContext(); await loadAdvisoryPolicy(); const context = saveContext(); @@ -100,10 +104,13 @@ async function showConsole() { loadActionHistory(); loadLearning(); } + runDiagnostics(); } function endSession(message = "Сессия завершена.") { sessionToken = ""; + clearInterval(refreshTimer); + refreshTimer = null; sessionStorage.removeItem("domovoy.creator.session"); consoleView.hidden = true; loginView.hidden = false; @@ -160,10 +167,59 @@ contextForm.addEventListener("submit", (event) => { loadDashboard(); }); +function restoreSettings() { + try { + const saved = JSON.parse(localStorage.getItem(settingsStorageKey) || "{}"); + uiSettings = { + density: ["comfortable", "compact"].includes(saved.density) ? saved.density : "comfortable", + refresh: ["off", "30", "60", "300"].includes(saved.refresh) ? saved.refresh : "off", + motion: saved.motion === true, + }; + } catch (_) { + uiSettings = {density: "comfortable", refresh: "off", motion: false}; + } + byId("setting-density").value = uiSettings.density; + byId("setting-refresh").value = uiSettings.refresh; + byId("setting-motion").checked = uiSettings.motion; + applySettings(); +} + +function applySettings() { + document.documentElement.dataset.density = uiSettings.density; + document.documentElement.dataset.reducedMotion = uiSettings.motion ? "true" : "false"; + clearInterval(refreshTimer); + refreshTimer = null; + if (sessionToken && uiSettings.refresh !== "off") { + refreshTimer = setInterval(() => loadDashboard(), Number(uiSettings.refresh) * 1000); + } +} + +byId("settings-form").addEventListener("submit", (event) => { + event.preventDefault(); + uiSettings = { + density: byId("setting-density").value, + refresh: byId("setting-refresh").value, + motion: byId("setting-motion").checked, + }; + localStorage.setItem(settingsStorageKey, JSON.stringify(uiSettings)); + applySettings(); + setText("settings-saved", "Сохранено только в этом браузере"); + notify("Настройки кабинета сохранены."); +}); + +byId("reset-settings").addEventListener("click", () => { + uiSettings = {density: "comfortable", refresh: "off", motion: false}; + localStorage.removeItem(settingsStorageKey); + restoreSettings(); + setText("settings-saved", "Восстановлены значения по умолчанию"); + notify("Настройки сброшены."); +}); + byId("refresh-all").addEventListener("click", loadDashboard); byId("refresh-actions").addEventListener("click", loadActionHistory); byId("refresh-learning").addEventListener("click", loadLearning); byId("logout").addEventListener("click", () => endSession()); +byId("run-diagnostics").addEventListener("click", runDiagnostics); advisoryForm.addEventListener("submit", async (event) => { event.preventDefault(); @@ -258,6 +314,56 @@ async function loadDashboard() { setText("last-update", `Обновлено ${new Intl.DateTimeFormat("ru-RU", {hour: "2-digit", minute: "2-digit", second: "2-digit"}).format(new Date())}`); } +async function runDiagnostics() { + const list = byId("diagnostic-list"); + const summary = byId("diagnostic-summary"); + const checks = [ + ["Сессия", "identity", () => request("/api/session/current")], + ["Политика совета", "advisory", () => request("/api/advisory-policy")], + ["Когнитивная возможность", "capability", () => request("/api/capabilities/cognitive-proposal/health")], + ["Сегодня", "today", () => request("/api/today")], + ["Внимание", "attention", () => request("/api/attention")], + ["Действия", "actions", () => request("/api/actions?limit=1")], + ["Learning evidence", "learning", () => request("/api/learning/recent?limit=1")], + ]; + list.setAttribute("aria-busy", "true"); + list.replaceChildren(); + checks.forEach(([label, key]) => list.append(createDiagnosticItem(label, key, "loading"))); + setText("diagnostic-summary", "Проверяю…"); + const results = await Promise.all(checks.map(async ([label, key, run]) => { + const started = performance.now(); + try { + await run(); + return {label, key, state: "ok", detail: `Ответ получен · ${Math.round(performance.now() - started)} мс`}; + } catch (_) { + return {label, key, state: "error", detail: "Сервис или контекст недоступен"}; + } + })); + list.replaceChildren(...results.map((result) => createDiagnosticItem(result.label, result.key, result.state, result.detail))); + list.setAttribute("aria-busy", "false"); + const passed = results.filter((result) => result.state === "ok").length; + setText("diagnostic-summary", `${passed}/${results.length} проверок пройдено`); +} + +function createDiagnosticItem(label, key, state, detail = "Проверяю…") { + const item = document.createElement("article"); + item.className = "diagnostic-item"; + item.dataset.state = state; + item.dataset.check = key; + const marker = document.createElement("i"); + marker.setAttribute("aria-hidden", "true"); + const copy = document.createElement("div"); + const title = document.createElement("strong"); + title.textContent = label; + const description = document.createElement("span"); + description.textContent = detail; + copy.append(title, description); + const status = document.createElement("em"); + status.textContent = state === "ok" ? "OK" : state === "error" ? "ошибка" : "…"; + item.append(marker, copy, status); + return item; +} + async function loadLearning() { const experienceList = byId("experience-list"); const reflectionList = byId("reflection-list"); diff --git a/cmd/creator-web/static/index.html b/cmd/creator-web/static/index.html index 86e9619..aa1ab78 100644 --- a/cmd/creator-web/static/index.html +++ b/cmd/creator-web/static/index.html @@ -44,6 +44,12 @@
система доступна +
@@ -68,6 +74,12 @@ +
ACTOR — @@ -77,7 +89,7 @@
-
+

операционная картина

Состояние сейчас

@@ -92,6 +104,20 @@
ВОЗМОЖНОСТЬожидает
+
+
+

Проверяемая система

Диагностика кабинета

+
Проверка ещё не запускалась
+
+
+ +

Здесь видны только ответы текущей авторизованной сессии. Проверка не меняет данные и помогает отличить отсутствие контекста от недоступности сервиса.

+
+
+

Нажмите «Проверить всё», чтобы получить актуальный снимок.

+
+
+

COGNITIVE / ADVISORY V1

@@ -120,7 +146,7 @@
-
+

Continuity / ближайшие

Реестр ожиданий

0 ЗАПИСЕЙ @@ -167,6 +193,19 @@
+
+
+

Локальные предпочтения

Настройки рабочего кабинета

+ Сохраняются в этом браузере +
+
+ + + +
+
+
+
01

Situation

Активный фокус

НЕ ЗАГРУЖЕН
diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 268f982..2a3d773 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000073-production-compose-fail-closed.json", - "task_id": "DEV-000073", + "task_file": "dev/tasks/DEV-000074-creator-cabinet-operations.json", + "task_id": "DEV-000074", "task_revision": 1 } diff --git a/dev/tasks/DEV-000074-creator-cabinet-operations.json b/dev/tasks/DEV-000074-creator-cabinet-operations.json new file mode 100644 index 0000000..d4db07e --- /dev/null +++ b/dev/tasks/DEV-000074-creator-cabinet-operations.json @@ -0,0 +1,92 @@ +{ + "task_id": "DEV-000074", + "task_revision": 1, + "architecture_revision": "DEV-000073-baseline", + "base_repo_revision": "407275c", + "risk_tier": "E", + "intent": "Make the authenticated Creator Web cabinet understandable and operable for a human by adding section navigation, read-only diagnostics, and browser-local interface preferences without changing domain state or authority boundaries.", + "requirement_ids": [ + "Human can move between overview, diagnostics, operations and settings", + "Human can run all currently exposed read-only Creator checks from one diagnostic panel", + "Interface preferences remain local to the browser and never change canonical state", + "Existing explicit confirmation remains required for canonical mutations", + "Desktop and mobile layouts remain usable" + ], + "allowed_change_scope": { + "modules": ["cmd/creator-web"], + "paths": [ + "cmd/creator-web/static/index.html", + "cmd/creator-web/static/assets/console.css", + "cmd/creator-web/static/assets/console.js", + "cmd/creator-web/server_test.go", + "cmd/creator-web/chat_test.go", + "docs/PROJECT_STATUS.md", + "dev/ACTIVE_TASK.json", + "dev/tasks/DEV-000074-creator-cabinet-operations.json" + ] + }, + "forbidden_change_scope": { + "paths": [ + "architecture/**", + "security/**", + "database/**", + "modules/**", + "contracts/**", + "adapters/**", + "cmd/core-runtime/**", + "cmd/commit-runtime/**", + "cmd/continuity-runtime/**", + "deploy/**" + ] + }, + "allowed_architecture_delta": { + "new_cross_module_dependencies": 0, + "new_canonical_owners": 0, + "new_privileges": 0, + "new_third_party_dependencies": 0, + "new_provider_adapters": 0, + "new_database_owner_roles": 0 + }, + "contracts": { + "consume": ["existing authenticated Creator gateway endpoints"], + "may_change": [] + }, + "semantic_owner": "none-interface-adapter", + "canonical_state": [], + "state_transitions": [ + "diagnostic request -> read-only gateway observation", + "settings form submit -> browser-local preference update" + ], + "side_effects_and_privileges": [ + "No new privilege or mutation endpoint", + "No database migration", + "Existing Commit actions retain explicit confirmation dialogs", + "No deployment or neighboring project mutation" + ], + "persistence_and_migrations": ["No canonical persistence; localStorage only for interface preferences"], + "failure_retry_restart": ["Diagnostic failures are shown per check without exposing upstream error details"], + "assumptions": [ + "Existing Creator API routes remain the source of truth", + "Browser-local settings are disposable and do not affect domain behavior" + ], + "acceptance_scenarios": [ + "Authenticated cabinet exposes navigation to all four human-facing sections", + "read-only diagnostics checks session, advisory policy, capability, today, attention, actions and learning", + "density, auto-refresh and reduced-motion preferences save and reset locally", + "mobile layout remains readable", + "existing Creator Web tests pass", + "architecture and diff scope checks pass" + ], + "required_tests": [ + "node --check cmd/creator-web/static/assets/console.js", + "go test ./cmd/creator-web", + "go test ./...", + "go vet ./...", + "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", + "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", + "go run ./cmd/devctl check-architecture", + "git diff --check", + "responsive browser inspection" + ], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"] +} -- 2.54.0 From 918748ca5708544038d54f55e35303d0af39d123 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 21 Aug 2026 02:49:57 +0300 Subject: [PATCH 23/32] docs: record Creator cabinet operations rollout --- docs/PROJECT_STATUS.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 6810e12..0d87c81 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -10,6 +10,15 @@ Audit date: 2026-08-17. Active implementation: DEV-000072 revision 1. - `PRODUCTION_ARTIFACT_AFTER_RECOVERY = revision 2e0c0236aa29d493d1ee9579354c244029cea09a on inspected Assistant containers; CURRENT_HEAD remains local and is not promoted` - `NEIGHBOR_SCOPE = read-only before/after snapshots showed atlas, llm-local, adapter-1c, gitea, portainer and Caddy still running; no neighbor container was targeted` +## Creator cabinet operations checkpoint — 2026-08-21 + +- `TASK = DEV-000074 Creator cabinet operations` +- `UI_CHANGE = human-facing navigation for overview, diagnostics, operations and settings; read-only gateway diagnostics; browser-local density, auto-refresh and reduced-motion preferences` +- `UI_VALIDATION = PASS: node --check console.js; go test ./cmd/creator-web; go test ./...; go vet ./...; devctl preflight; check-diff; check-architecture; git diff --check; browser console had no errors on static smoke` +- `PRODUCTION_PROMOTION = Assistant-only creator-web image built from revision 3ed64631f6a0defbf90c7981179f99d26de34ca9 and started with no dependency recreation` +- `PRODUCTION_UI_EVIDENCE = https://assistant.cin.su/ HTTP 200 and deployed HTML contains diagnostics/settings markers; creator-web healthy on 18080; healthz HTTP 200` +- `NEIGHBOR_SCOPE = atlas, llm-local, adapter-1c, gitea, portainer and Caddy remained running; no neighbor image or container was targeted` + ## Post-audit local repair checkpoint — 2026-08-21 - `CURRENT_BRANCH = codex/domovoy-understanding-repair-v1` -- 2.54.0 From 4226c986d70af017ecfbb8755e0d7a44bc189fa7 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 21 Aug 2026 03:15:07 +0300 Subject: [PATCH 24/32] fix: make Creator cabinet readable on wide screens --- cmd/creator-web/static/assets/console.css | 24 +++++++++++------------ cmd/creator-web/static/assets/console.js | 9 ++++++--- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/cmd/creator-web/static/assets/console.css b/cmd/creator-web/static/assets/console.css index 6fe659a..ab3858f 100644 --- a/cmd/creator-web/static/assets/console.css +++ b/cmd/creator-web/static/assets/console.css @@ -1,12 +1,12 @@ :root { - --ink: #e9eee7; - --muted: #8c9a91; - --faint: #58645d; - --base: #0b100e; - --panel: #101714; - --panel-high: #151e1a; - --line: #2a352f; - --line-soft: #1d2822; + --ink: #eef2ec; + --muted: #a7b2aa; + --faint: #718078; + --base: #0f1512; + --panel: #17201b; + --panel-high: #202b25; + --line: #34423a; + --line-soft: #26332c; --signal: #c9a85f; --signal-bright: #e7c97b; --good: #78c6a3; @@ -22,12 +22,12 @@ * { box-sizing: border-box; } [hidden] { display: none !important; } html { background: var(--base); color: var(--ink); font-family: var(--sans); } -body { margin: 0; min-width: 320px; min-height: 100vh; background: radial-gradient(circle at 73% -10%, rgba(83, 123, 101, .15), transparent 38%), var(--base); } +body { margin: 0; min-width: 320px; min-height: 100vh; background: radial-gradient(circle at 73% -10%, rgba(83, 123, 101, .13), transparent 38%), var(--base); } button, input, textarea { font: inherit; } button { color: inherit; } button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px solid var(--signal-bright); outline-offset: 3px; } -.grain { position: fixed; inset: 0; pointer-events: none; opacity: .18; z-index: 10; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 160 160' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E"); } +.grain { position: fixed; inset: 0; pointer-events: none; opacity: .045; z-index: 10; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 160 160' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E"); } .eyebrow, .rail-index { margin: 0 0 12px; color: var(--signal); font: 600 11px/1 var(--mono); letter-spacing: .18em; text-transform: uppercase; } .login-shell { min-height: 100vh; display: grid; place-items: center; padding: 32px 20px; position: relative; overflow: hidden; } @@ -90,9 +90,9 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px .session-stamp span { margin-top: 8px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .14em; } .session-stamp strong { overflow: hidden; color: var(--muted); font: 10px/1.4 var(--mono); text-overflow: ellipsis; } -.workspace { grid-column: 2; grid-row: 2; padding: clamp(28px, 4vw, 58px); min-width: 0; } +.workspace { grid-column: 2; grid-row: 2; width: min(100%, 1740px); justify-self: start; padding: 42px clamp(24px, 3vw, 48px) 64px; min-width: 0; } .workspace-head { display: flex; align-items: end; justify-content: space-between; gap: 24px; margin-bottom: 34px; } -.workspace-head h1 { margin: 0; font: 400 clamp(36px, 5vw, 58px)/1 var(--serif); letter-spacing: -.035em; } +.workspace-head h1 { margin: 0; font: 400 clamp(34px, 4vw, 50px)/1 var(--serif); letter-spacing: -.035em; } .last-update { color: var(--faint); font: 10px/1.4 var(--mono); text-align: right; } .signal-strip { display: grid; grid-template-columns: repeat(4, 1fr); margin-bottom: 18px; border: 1px solid var(--line); background: var(--panel); } .signal-strip div { padding: 16px 18px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border-right: 1px solid var(--line); } diff --git a/cmd/creator-web/static/assets/console.js b/cmd/creator-web/static/assets/console.js index 3eb6830..8e88043 100644 --- a/cmd/creator-web/static/assets/console.js +++ b/cmd/creator-web/static/assets/console.js @@ -65,7 +65,9 @@ async function request(path, options = {}) { } if (!response.ok) { const detail = (await response.text()).trim(); - throw new Error(detail || `Ошибка ${response.status}`); + const error = new Error(detail || `Ошибка ${response.status}`); + error.status = response.status; + throw error; } return response.json(); } @@ -335,8 +337,9 @@ async function runDiagnostics() { try { await run(); return {label, key, state: "ok", detail: `Ответ получен · ${Math.round(performance.now() - started)} мс`}; - } catch (_) { - return {label, key, state: "error", detail: "Сервис или контекст недоступен"}; + } catch (error) { + const detail = error?.status === 400 ? "Нужны параметры рабочего контекста" : error?.status === 401 ? "Сессия истекла" : error?.status >= 500 ? "Сервис временно недоступен" : "Проверка не пройдена"; + return {label, key, state: "error", detail}; } })); list.replaceChildren(...results.map((result) => createDiagnosticItem(result.label, result.key, result.state, result.detail))); -- 2.54.0 From 8397e69c2fcb525fc410bf2350e5d6d66882ba91 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 21 Aug 2026 03:16:18 +0300 Subject: [PATCH 25/32] docs: record cabinet readability hotfix --- docs/PROJECT_STATUS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 0d87c81..8fc4354 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -18,6 +18,7 @@ Audit date: 2026-08-17. Active implementation: DEV-000072 revision 1. - `PRODUCTION_PROMOTION = Assistant-only creator-web image built from revision 3ed64631f6a0defbf90c7981179f99d26de34ca9 and started with no dependency recreation` - `PRODUCTION_UI_EVIDENCE = https://assistant.cin.su/ HTTP 200 and deployed HTML contains diagnostics/settings markers; creator-web healthy on 18080; healthz HTTP 200` - `NEIGHBOR_SCOPE = atlas, llm-local, adapter-1c, gitea, portainer and Caddy remained running; no neighbor image or container was targeted` +- `VISUAL_HOTFIX = 2026-08-21: constrained workspace to 1740px on wide screens, raised contrast and reduced grain, made diagnostics explain HTTP failure classes; creator-web revision 4226c986d70af017ecfbb8755e0d7a44bc189fa is healthy` ## Post-audit local repair checkpoint — 2026-08-21 -- 2.54.0 From 5789b70e9c4d4fa1a1c6e26a1126d79568fe2389 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 21 Aug 2026 03:18:27 +0300 Subject: [PATCH 26/32] docs: record cabinet diagnostic recovery --- docs/PROJECT_STATUS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 8fc4354..d0f1cef 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -19,6 +19,7 @@ Audit date: 2026-08-17. Active implementation: DEV-000072 revision 1. - `PRODUCTION_UI_EVIDENCE = https://assistant.cin.su/ HTTP 200 and deployed HTML contains diagnostics/settings markers; creator-web healthy on 18080; healthz HTTP 200` - `NEIGHBOR_SCOPE = atlas, llm-local, adapter-1c, gitea, portainer and Caddy remained running; no neighbor image or container was targeted` - `VISUAL_HOTFIX = 2026-08-21: constrained workspace to 1740px on wide screens, raised contrast and reduced grain, made diagnostics explain HTTP failure classes; creator-web revision 4226c986d70af017ecfbb8755e0d7a44bc189fa is healthy` +- `DIAGNOSTIC_RECOVERY = 2026-08-21: synchronized eight Assistant PostgreSQL role passwords from the ignored production bootstrap secret without changing data; authenticated session, advisory policy, capability health, today, attention, actions and learning endpoints all returned HTTP 200` ## Post-audit local repair checkpoint — 2026-08-21 -- 2.54.0 From 406a106e30dbb428279ef21b30729eeefca39d7a Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 21 Aug 2026 03:19:54 +0300 Subject: [PATCH 27/32] fix: bust Creator cabinet asset cache --- cmd/creator-web/static/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/creator-web/static/index.html b/cmd/creator-web/static/index.html index aa1ab78..c197a43 100644 --- a/cmd/creator-web/static/index.html +++ b/cmd/creator-web/static/index.html @@ -6,7 +6,7 @@ Домовой — консоль создателя - + @@ -325,6 +325,6 @@ - + -- 2.54.0 From 93b0965485e8ba9ca85613a71d137f3f35203376 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 21 Aug 2026 03:21:48 +0300 Subject: [PATCH 28/32] feat: simplify human context setup --- cmd/creator-web/static/assets/console.css | 10 ++++++++++ cmd/creator-web/static/assets/console.js | 7 +++++++ cmd/creator-web/static/index.html | 24 ++++++++++++++--------- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/cmd/creator-web/static/assets/console.css b/cmd/creator-web/static/assets/console.css index ab3858f..3cd4feb 100644 --- a/cmd/creator-web/static/assets/console.css +++ b/cmd/creator-web/static/assets/console.css @@ -79,6 +79,16 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px .context-form label { margin-top: 15px; } .context-form label:first-child { margin-top: 0; } .context-form input { height: 40px; padding: 0 10px; font: 11px/1 var(--mono); } +.context-hint { margin: 12px 0 0; color: var(--faint); font-size: 10px; line-height: 1.45; } +.advanced-context { margin-top: 17px; border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); } +.advanced-context summary { padding: 12px 0; color: var(--muted); cursor: pointer; list-style: none; font: 600 9px/1.2 var(--mono); letter-spacing: .08em; text-transform: uppercase; } +.advanced-context summary::before { content: "+"; display: inline-block; width: 18px; color: var(--signal); font-size: 13px; } +.advanced-context[open] summary::before { content: "−"; } +.advanced-context > div { padding: 0 0 10px; } +.advanced-context label { margin-top: 13px; } +.context-actions { display: grid; gap: 8px; } +.context-actions .secondary-button { margin-top: 18px; } +.context-actions .text-button { margin: 0; justify-self: start; padding-inline: 0; } .secondary-button { height: 42px; margin-top: 20px; color: #11150f; background: var(--signal); font-size: 12px; } .section-nav { display: grid; gap: 3px; margin-top: 4px; } .section-nav-link { display: grid; grid-template-columns: 28px 1fr; gap: 2px 10px; padding: 11px 10px; border-left: 2px solid transparent; color: var(--muted); text-decoration: none; transition: background .18s, border-color .18s, color .18s; } diff --git a/cmd/creator-web/static/assets/console.js b/cmd/creator-web/static/assets/console.js index 8e88043..2fbf0a2 100644 --- a/cmd/creator-web/static/assets/console.js +++ b/cmd/creator-web/static/assets/console.js @@ -169,6 +169,13 @@ contextForm.addEventListener("submit", (event) => { loadDashboard(); }); +byId("reset-context").addEventListener("click", () => { + contextFields.forEach((id) => { byId(id).value = id === "capability-id" ? "cognitive-proposal" : ""; }); + localStorage.removeItem("domovoy.creator.context"); + loadDashboard(); + notify("Рабочий контекст сброшен."); +}); + function restoreSettings() { try { const saved = JSON.parse(localStorage.getItem(settingsStorageKey) || "{}"); diff --git a/cmd/creator-web/static/index.html b/cmd/creator-web/static/index.html index c197a43..ac461e5 100644 --- a/cmd/creator-web/static/index.html +++ b/cmd/creator-web/static/index.html @@ -6,7 +6,7 @@ Домовой — консоль создателя - + @@ -66,13 +66,19 @@ - - - - - - - +

Сессия и рабочая область нужны для основного обзора. Остальные поля — для точечной проверки.

+
+ Расширенные идентификаторы +
+ + + + + + +
+
+
@@ -266,6 +267,7 @@
Ревизия
—

Укажите идентификатор ProspectiveItem для проверки владельца и состояния.

+

Commit / контролируемый переход

@@ -301,6 +303,7 @@
Ревизия
—
Наблюдалось
—
+
-- 2.54.0