| 1 | // Goal budget helpers classify natural-language task text (English and |
| 2 | // Chinese) into delivery-intent categories. The delivery evidence gates and |
| 3 | // Goal budget selection consume it as a heuristic; it never gates permissions |
| 4 | // or whether writes are allowed. |
| 5 | package control |
| 6 | |
| 7 | import ( |
| 8 | "strings" |
| 9 | "unicode/utf8" |
| 10 | ) |
| 11 | |
| 12 | // Intent is the delivery expectation a task text implies. |
| 13 | type Intent uint8 |
| 14 | |
| 15 | const ( |
| 16 | // Conversation is chat with no host-observable work expected. |
| 17 | Conversation Intent = iota |
| 18 | // Advisory asks for explanation or advice rather than observable work. |
| 19 | Advisory |
| 20 | // ObservableRead expects host-observable read-only work (inspect, review). |
| 21 | ObservableRead |
| 22 | // Mutation expects workspace changes. |
| 23 | Mutation |
| 24 | // PersistentAction expects durable state kept across sessions. |
| 25 | PersistentAction |
| 26 | ) |
| 27 | |
| 28 | // Classify maps a task text to the delivery intent it implies. |
| 29 | func Classify(input string) Intent { |
| 30 | switch { |
| 31 | case deliveryTaskHasMutationIntent(input): |
| 32 | return Mutation |
| 33 | case NeedsPersistentAction(input): |
| 34 | return PersistentAction |
| 35 | case deliveryTaskIsConversationOnly(input): |
| 36 | return Conversation |
| 37 | case !heuristicInputIsTask(input): |
| 38 | return Conversation |
| 39 | case deliveryTaskIsAdvisory(input): |
| 40 | return Advisory |
| 41 | default: |
| 42 | return ObservableRead |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | // NeedsEvidence reports whether the intent expects host-observable work. |
| 47 | func (i Intent) NeedsEvidence() bool { |
| 48 | return i == ObservableRead || i == Mutation || i == PersistentAction |
| 49 | } |
| 50 | |
| 51 | // NeedsEvidence reports whether a task text expects host-observable work. |
| 52 | func NeedsEvidence(input string) bool { |
| 53 | return Classify(input).NeedsEvidence() |
| 54 | } |
| 55 | |
| 56 | var deliveryMutationNeedles = []string{ |
| 57 | "fix", "repair", "resolve", "create", "add", "write", "edit", "update", "change", "delete", "remove", "rename", |
| 58 | "implement", "refactor", "apply", "install", "publish", "commit", "push", "continue work", |
| 59 | "modify", "patch", "replace", "move", "configure", "upgrade", "downgrade", "bump", "enable", "disable", "merge", |
| 60 | "make changes", "make a change", "make the changes", "make the requested changes", "make the necessary changes", "make these changes", "make those changes", "make code changes", |
| 61 | "修复", "解决", "创建", "新建", "添加", "编写", "编辑", "修改", "更新", "删除", "移除", "重命名", "实现", "重构", |
| 62 | "实施", "落地", "安装", "发布", "提交", "继续处理", "调整", "替换", "移动", "升级", "降级", "启用", "禁用", "合并", "改动", "打补丁", |
| 63 | } |
| 64 | |
| 65 | var deliveryAdvisoryPhrases = []string{ |
| 66 | "what's wrong", "what is wrong", "why", "what should i do", "what can i do", "how should i", "how do i", "how can i", |
| 67 | "can you explain", "could you explain", "give me advice", "any advice", "help me understand", |
| 68 | "为什么", "怎么回事", "怎么办", "怎么", "怎样", "如何", "是什么问题", "什么原因", "的原因", "给我建议", "有什么建议", |
| 69 | } |
| 70 | |
| 71 | func NeedsMutation(input string) bool { |
| 72 | intent := Classify(input) |
| 73 | return intent == Mutation || intent == PersistentAction |
| 74 | } |
| 75 | |
| 76 | func deliveryTaskHasMutationIntent(input string) bool { |
| 77 | affirmative, _ := deliveryTaskMutationIntent(input) |
| 78 | return affirmative |
| 79 | } |
| 80 | |
| 81 | func NeedsPersistentAction(input string) bool { |
| 82 | normalized := strings.ToLower(strings.TrimSpace(input)) |
| 83 | if normalized == "" { |
| 84 | return false |
| 85 | } |
| 86 | actionNeedles := []string{ |
| 87 | "remember", "save", "store", "keep this", "keep that", |
| 88 | "记住", "记下来", "保存", "存下来", "记录下来", |
| 89 | } |
| 90 | durableNeedles := []string{ |
| 91 | "permanently", "durable", "long-term", "long term", "across sessions", "future sessions", "every session", "after restart", "after restarting", |
| 92 | "永久", "长期", "持久", "跨会话", "以后每次", "未来会话", "重启后", "下次启动", |
| 93 | } |
| 94 | for _, clause := range deliveryTaskClauses(normalized) { |
| 95 | action := false |
| 96 | for _, needle := range actionNeedles { |
| 97 | affirmative, _ := deliveryTaskNeedleIntent(clause, needle) |
| 98 | action = action || affirmative |
| 99 | } |
| 100 | durable := false |
| 101 | for _, needle := range durableNeedles { |
| 102 | affirmative, _ := deliveryTaskNeedleIntent(clause, needle) |
| 103 | durable = durable || affirmative |
| 104 | } |
| 105 | if action && durable && !deliveryTaskClauseIsAdvisory(clause) { |
| 106 | return true |
| 107 | } |
| 108 | } |
| 109 | return false |
| 110 | } |
| 111 | |
| 112 | func deliveryTaskIsConversationOnly(input string) bool { |
| 113 | normalized := strings.ToLower(strings.TrimSpace(input)) |
| 114 | if normalized == "" || deliveryTaskHasHostAnchor(normalized) || deliveryTaskHasCommand(normalized) { |
| 115 | return false |
| 116 | } |
| 117 | localCue := containsAnySubstring(normalized, []string{ |
| 118 | "next turn", "next message", "later in this chat", "this conversation", "when i ask again", "when i ask next", |
| 119 | "下一轮", "下轮", "下一条消息", "稍后再问", "待会再问", "这个对话", "本次对话", "本轮会话", |
| 120 | }) |
| 121 | conversationAction := containsAnySubstring(normalized, []string{ |
| 122 | "remember", "keep in mind", "keep this", "keep that", "answer", "respond", "reply", |
| 123 | "记住", "记一下", "回答", "回复", "再告诉我", |
| 124 | }) |
| 125 | return localCue && conversationAction |
| 126 | } |
| 127 | |
| 128 | func deliveryTaskMutationIntent(input string) (affirmative, negated bool) { |
| 129 | normalized := strings.ToLower(strings.TrimSpace(input)) |
| 130 | for _, clause := range deliveryTaskClauses(normalized) { |
| 131 | clauseAffirmative := false |
| 132 | clauseNegated := false |
| 133 | if deliveryMutationClauseNegated(clause) { |
| 134 | clauseNegated = true |
| 135 | } |
| 136 | for _, needle := range deliveryMutationNeedles { |
| 137 | hasAffirmative, hasNegated := deliveryTaskNeedleIntent(clause, needle) |
| 138 | clauseAffirmative = clauseAffirmative || hasAffirmative |
| 139 | clauseNegated = clauseNegated || hasNegated |
| 140 | } |
| 141 | if clauseAffirmative && deliveryTaskClauseIsAdvisory(clause) && !deliveryTaskAdvisoryClauseRequestsMutation(clause) { |
| 142 | clauseAffirmative = false |
| 143 | clauseNegated = true |
| 144 | } |
| 145 | affirmative = affirmative || clauseAffirmative |
| 146 | negated = negated || clauseNegated |
| 147 | } |
| 148 | return affirmative, negated |
| 149 | } |
| 150 | |
| 151 | func deliveryTaskIsAdvisory(input string) bool { |
| 152 | normalized := strings.ToLower(strings.TrimSpace(input)) |
| 153 | |
| 154 | // Concrete targets and commands always remain host-observable, including |
| 155 | // when the request is phrased as a "why" question. |
| 156 | if deliveryTaskHasHostAnchor(normalized) || deliveryTaskHasCommand(normalized) { |
| 157 | return false |
| 158 | } |
| 159 | |
| 160 | // Question wording is scoped per clause. This keeps remote troubleshooting |
| 161 | // such as "analyze why WPS won't open" advisory, while a separate imperative |
| 162 | // clause such as "reproduce the crash" still requires observable work. |
| 163 | sawAdvisory := false |
| 164 | for _, clause := range deliveryTaskClauses(normalized) { |
| 165 | if deliveryTaskClauseIsAdvisory(clause) { |
| 166 | sawAdvisory = true |
| 167 | continue |
| 168 | } |
| 169 | if deliveryTaskClauseHasObservableWork(clause) { |
| 170 | return false |
| 171 | } |
| 172 | } |
| 173 | if sawAdvisory { |
| 174 | return true |
| 175 | } |
| 176 | |
| 177 | // A standalone refusal, inability, or constraint around a mutation verb is |
| 178 | // advisory rather than work Reasonix can perform. Affirmative mixed intent is |
| 179 | // handled by NeedsMutation before this function is consulted. |
| 180 | _, negatedMutation := deliveryTaskMutationIntent(normalized) |
| 181 | return negatedMutation |
| 182 | } |
| 183 | |
| 184 | func deliveryTaskHasHostAnchor(input string) bool { |
| 185 | for _, anchor := range []string{ |
| 186 | "this repo", "this repository", "current repository", "codebase", "workspace", "pull request", "this pr", "ci job", |
| 187 | "/pull/", "actions/runs/", |
| 188 | "当前仓库", "这个仓库", "当前项目", "这个项目", "代码库", "工作区", "这个 pr", "这个pr", "此 pr", "此pr", |
| 189 | } { |
| 190 | if strings.Contains(input, anchor) { |
| 191 | return true |
| 192 | } |
| 193 | } |
| 194 | return deliveryTaskHasFileReference(input) |
| 195 | } |
| 196 | |
| 197 | func deliveryTaskHasFileReference(input string) bool { |
| 198 | previous := rune(0) |
| 199 | for index, current := range input { |
| 200 | if current == '@' && index+1 < len(input) && |
| 201 | (index == 0 || strings.ContainsRune(" \t\r\n([{<,:;(【《,。;:", previous)) { |
| 202 | next, _ := utf8.DecodeRuneInString(input[index+1:]) |
| 203 | if !strings.ContainsRune(" \t\r\n", next) { |
| 204 | return true |
| 205 | } |
| 206 | } |
| 207 | previous = current |
| 208 | } |
| 209 | |
| 210 | for _, raw := range strings.FieldsFunc(input, func(r rune) bool { |
| 211 | switch r { |
| 212 | case ' ', '\t', '\r', '\n', '`', '\'', '"', '(', ')', '[', ']', '{', '}', '<', '>', ',', ',', ';', ';', '!', '!', '?', '?': |
| 213 | return true |
| 214 | default: |
| 215 | return false |
| 216 | } |
| 217 | }) { |
| 218 | token := strings.ToLower(strings.TrimSpace(raw)) |
| 219 | if token == "" || strings.Contains(token, "://") { |
| 220 | continue |
| 221 | } |
| 222 | if strings.HasPrefix(token, "./") || strings.HasPrefix(token, "../") || |
| 223 | strings.HasPrefix(token, "/") || strings.Contains(token, `\`) { |
| 224 | return true |
| 225 | } |
| 226 | base := token |
| 227 | if slash := strings.LastIndexByte(base, '/'); slash >= 0 { |
| 228 | base = base[slash+1:] |
| 229 | } |
| 230 | switch base { |
| 231 | case "dockerfile", "makefile", "cmakelists.txt", "justfile", "license", "readme", "changelog": |
| 232 | return true |
| 233 | } |
| 234 | dot := strings.LastIndexByte(base, '.') |
| 235 | if dot < 0 { |
| 236 | continue |
| 237 | } |
| 238 | switch base[dot:] { |
| 239 | case ".go", ".mod", ".sum", ".js", ".jsx", ".ts", ".tsx", ".py", ".rs", ".java", ".kt", ".swift", |
| 240 | ".c", ".cc", ".cpp", ".h", ".hpp", ".cs", ".rb", ".php", ".sh", ".zsh", ".fish", ".ps1", |
| 241 | ".md", ".json", ".yaml", ".yml", ".toml", ".xml", ".sql", ".proto", ".html", ".css", ".scss", |
| 242 | ".vue", ".svelte", ".txt", ".log", ".csv", ".pdf", ".env", ".ini", ".conf", ".lock": |
| 243 | return true |
| 244 | } |
| 245 | } |
| 246 | return false |
| 247 | } |
| 248 | |
| 249 | func deliveryTaskHasCommand(input string) bool { |
| 250 | tokens := strings.FieldsFunc(strings.ToLower(input), func(r rune) bool { |
| 251 | asciiWord := r >= 'a' && r <= 'z' || r >= '0' && r <= '9' |
| 252 | return !asciiWord && r != '_' && r != '-' && r != '.' && r != '/' && r != '\\' && r != ':' |
| 253 | }) |
| 254 | for i := range tokens { |
| 255 | if deliveryCommandStartsAt(tokens, i) { |
| 256 | return true |
| 257 | } |
| 258 | } |
| 259 | return false |
| 260 | } |
| 261 | |
| 262 | func deliveryCommandStartsAt(tokens []string, index int) bool { |
| 263 | command := strings.TrimSpace(tokens[index]) |
| 264 | if command == "" { |
| 265 | return false |
| 266 | } |
| 267 | if strings.HasPrefix(command, "./") || strings.HasPrefix(command, "../") || |
| 268 | strings.HasPrefix(command, "/") || strings.Contains(command, `\`) { |
| 269 | return true |
| 270 | } |
| 271 | next := "" |
| 272 | if index+1 < len(tokens) { |
| 273 | next = tokens[index+1] |
| 274 | } |
| 275 | if next != "--" && len(next) > 1 && strings.HasPrefix(next, "-") { |
| 276 | return true |
| 277 | } |
| 278 | previous := "" |
| 279 | if index > 0 { |
| 280 | previous = tokens[index-1] |
| 281 | } |
| 282 | switch command { |
| 283 | case "go": |
| 284 | switch next { |
| 285 | case "build", "clean", "doc", "env", "fmt", "generate", "get", "install", "list", "mod", "run", "test", "tool", "version", "vet", "work": |
| 286 | return true |
| 287 | } |
| 288 | case "git", "npm", "npx", "pnpm", "yarn", "bun", "deno", "cargo", "rustc", "python", "python3", |
| 289 | "bash", "sh", "zsh", "fish", "powershell", "pwsh", "docker", "docker-compose", "kubectl", "helm", "terraform", |
| 290 | "gradle", "gradlew", "mvn", "dotnet", "xcodebuild", "gcc", "g++", "clang", "clang++": |
| 291 | return deliveryCommandHasExplicitCue(previous) || deliveryCommandHasSubcommand(next) |
| 292 | case "node": |
| 293 | return deliveryCommandHasExplicitCue(previous) || next == "inspect" || next == "test" |
| 294 | case "swift": |
| 295 | return next == "build" || next == "package" || next == "run" || next == "test" |
| 296 | case "make", "just": |
| 297 | switch next { |
| 298 | case "all", "build", "check", "clean", "fail", "failed", "failing", "install", "lint", "test": |
| 299 | return true |
| 300 | } |
| 301 | case "pytest", "cmake", "ninja", "eslint", "tsc", "vitest", "jest": |
| 302 | return deliveryCommandHasExplicitCue(previous) || next == "fail" || next == "failed" || next == "failing" |
| 303 | } |
| 304 | return false |
| 305 | } |
| 306 | |
| 307 | func deliveryCommandHasExplicitCue(previous string) bool { |
| 308 | switch previous { |
| 309 | case "command", "execute", "executing", "run", "running", "using", "with": |
| 310 | return true |
| 311 | default: |
| 312 | return false |
| 313 | } |
| 314 | } |
| 315 | |
| 316 | func deliveryCommandHasSubcommand(next string) bool { |
| 317 | switch next { |
| 318 | case "add", "apply", "branch", "build", "check", "checkout", "clean", "clone", "commit", "config", "container", |
| 319 | "deploy", "describe", "destroy", "dev", "diff", "down", "env", "exec", "fetch", "fmt", "generate", "get", "image", |
| 320 | "init", "install", "lint", "list", "log", "logs", "login", "logout", "merge", "mod", "package", "plan", "ps", "publish", |
| 321 | "pull", "push", "rebase", "remote", "remove", "reset", "restore", "run", "serve", "show", "start", "stash", "status", |
| 322 | "switch", "tag", "test", "tool", "uninstall", "up", "update", "upgrade", "version", "vet", "work", "worktree": |
| 323 | return true |
| 324 | default: |
| 325 | return false |
| 326 | } |
| 327 | } |
| 328 | |
| 329 | func deliveryTaskClauseHasObservableWork(clause string) bool { |
| 330 | for _, needle := range []string{ |
| 331 | "review", "inspect", "analyze", "check", "reproduce", "audit", "verify", |
| 332 | "评审", "审查", "检查", "分析", "复现", "审计", "验证", |
| 333 | } { |
| 334 | affirmative, _ := deliveryTaskNeedleIntent(clause, needle) |
| 335 | if affirmative { |
| 336 | return true |
| 337 | } |
| 338 | } |
| 339 | return false |
| 340 | } |
| 341 | |
| 342 | func deliveryTaskClauseIsAdvisory(clause string) bool { |
| 343 | for _, phrase := range deliveryAdvisoryPhrases { |
| 344 | if strings.Contains(clause, phrase) { |
| 345 | return true |
| 346 | } |
| 347 | } |
| 348 | return false |
| 349 | } |
| 350 | |
| 351 | func deliveryTaskAdvisoryClauseRequestsMutation(clause string) bool { |
| 352 | advisoryIndex := len(clause) |
| 353 | for _, phrase := range deliveryAdvisoryPhrases { |
| 354 | if index := strings.Index(clause, phrase); index >= 0 && index < advisoryIndex { |
| 355 | advisoryIndex = index |
| 356 | } |
| 357 | } |
| 358 | if advisoryIndex == len(clause) { |
| 359 | return false |
| 360 | } |
| 361 | if deliveryTaskStartsWithMutation(clause[:advisoryIndex]) { |
| 362 | return true |
| 363 | } |
| 364 | |
| 365 | for _, cue := range []string{" please ", " then ", " so ", " therefore ", "然后", "所以", "而是", "转而"} { |
| 366 | for rest := clause[advisoryIndex:]; ; { |
| 367 | index := strings.Index(rest, cue) |
| 368 | if index < 0 { |
| 369 | break |
| 370 | } |
| 371 | rest = rest[index+len(cue):] |
| 372 | if deliveryTaskStartsWithMutation(rest) { |
| 373 | return true |
| 374 | } |
| 375 | } |
| 376 | } |
| 377 | for rest, offset := clause[advisoryIndex:], advisoryIndex; ; { |
| 378 | index := strings.Index(rest, "请") |
| 379 | if index < 0 { |
| 380 | break |
| 381 | } |
| 382 | absolute := offset + index |
| 383 | after := clause[absolute+len("请"):] |
| 384 | requestWord := strings.HasSuffix(clause[:absolute], "申") || strings.HasPrefix(after, "求") |
| 385 | if !requestWord && deliveryTaskStartsWithMutation(after) { |
| 386 | return true |
| 387 | } |
| 388 | offset = absolute + len("请") |
| 389 | rest = clause[offset:] |
| 390 | } |
| 391 | |
| 392 | for _, cue := range []string{" and ", "并且", "并"} { |
| 393 | if index := strings.LastIndex(clause[advisoryIndex:], cue); index >= 0 { |
| 394 | cueStart := advisoryIndex + index |
| 395 | tail := clause[cueStart+len(cue):] |
| 396 | if !deliveryTaskClauseHasNegation(clause[:cueStart]) && deliveryTaskStartsWithMutation(tail) { |
| 397 | return true |
| 398 | } |
| 399 | } |
| 400 | } |
| 401 | return false |
| 402 | } |
| 403 | |
| 404 | func deliveryTaskStartsWithMutation(input string) bool { |
| 405 | input = strings.TrimSpace(input) |
| 406 | for { |
| 407 | stripped := false |
| 408 | for _, prefix := range []string{"please ", "can you ", "could you ", "would you ", "you should ", "帮我", "请你", "直接", "继续", "再"} { |
| 409 | if after, ok := strings.CutPrefix(input, prefix); ok { |
| 410 | input = strings.TrimSpace(after) |
| 411 | stripped = true |
| 412 | break |
| 413 | } |
| 414 | } |
| 415 | if !stripped { |
| 416 | break |
| 417 | } |
| 418 | } |
| 419 | for _, needle := range deliveryMutationNeedles { |
| 420 | if containsTaskNeedle(input, needle) { |
| 421 | if goalBudgetContainsNonASCII(needle) { |
| 422 | return strings.HasPrefix(input, needle) |
| 423 | } |
| 424 | tokens := strings.FieldsFunc(input, func(r rune) bool { |
| 425 | return !(r >= 'a' && r <= 'z') && !(r >= '0' && r <= '9') && r != '_' && r != '\'' |
| 426 | }) |
| 427 | needleTokens := strings.Fields(needle) |
| 428 | if len(tokens) >= len(needleTokens) { |
| 429 | matches := true |
| 430 | for i := range needleTokens { |
| 431 | matches = matches && tokens[i] == needleTokens[i] |
| 432 | } |
| 433 | if matches { |
| 434 | return true |
| 435 | } |
| 436 | } |
| 437 | } |
| 438 | } |
| 439 | return false |
| 440 | } |
| 441 | |
| 442 | func deliveryTaskClauseHasNegation(clause string) bool { |
| 443 | clause = strings.ReplaceAll(clause, "’", "'") |
| 444 | for _, phrase := range []string{ |
| 445 | " not ", " never ", " without ", "cannot", "can't", " cant ", "don't", " dont ", "won't", " wont ", "unable", |
| 446 | "不要", "别", "勿", "不能", "无法", "不想", "不敢", "无需", "不需要", "不可", "没法", "没有", "禁止", "拒绝", |
| 447 | } { |
| 448 | if strings.Contains(" "+clause+" ", phrase) { |
| 449 | return true |
| 450 | } |
| 451 | } |
| 452 | return false |
| 453 | } |
| 454 | |
| 455 | func deliveryTaskClauses(input string) []string { |
| 456 | input = strings.NewReplacer( |
| 457 | " but ", "\n", |
| 458 | " however ", "\n", |
| 459 | " nevertheless ", "\n", |
| 460 | "但请", "\n请", |
| 461 | "但是", "\n", |
| 462 | "不过", "\n", |
| 463 | ).Replace(input) |
| 464 | return strings.FieldsFunc(input, func(r rune) bool { |
| 465 | switch r { |
| 466 | case '\n', '\r', '.', '。', ',', ',', ';', ';', '!', '!', '?', '?': |
| 467 | return true |
| 468 | default: |
| 469 | return false |
| 470 | } |
| 471 | }) |
| 472 | } |
| 473 | |
| 474 | func deliveryMutationClauseNegated(clause string) bool { |
| 475 | for _, phrase := range []string{ |
| 476 | "without changing", "without modifying", "analysis only", "review only", |
| 477 | "不要改动", "只分析", "仅分析", "只检查", "仅检查", "只评审", "仅评审", |
| 478 | } { |
| 479 | if strings.Contains(clause, phrase) { |
| 480 | return true |
| 481 | } |
| 482 | } |
| 483 | return false |
| 484 | } |
| 485 | |
| 486 | func deliveryTaskNeedleIntent(clause, needle string) (affirmative, negated bool) { |
| 487 | if goalBudgetContainsNonASCII(needle) { |
| 488 | for offset := 0; offset < len(clause); { |
| 489 | relative := strings.Index(clause[offset:], needle) |
| 490 | if relative < 0 { |
| 491 | break |
| 492 | } |
| 493 | index := offset + relative |
| 494 | prefix := []rune(clause[:index]) |
| 495 | if deliveryMutationRunesNegated(prefix) { |
| 496 | negated = true |
| 497 | } else { |
| 498 | affirmative = true |
| 499 | } |
| 500 | offset = index + len(needle) |
| 501 | } |
| 502 | return affirmative, negated |
| 503 | } |
| 504 | |
| 505 | clause = strings.ReplaceAll(clause, "’", "'") |
| 506 | tokens := strings.FieldsFunc(clause, func(r rune) bool { |
| 507 | return !(r >= 'a' && r <= 'z') && !(r >= '0' && r <= '9') && r != '_' && r != '\'' |
| 508 | }) |
| 509 | needleTokens := strings.Fields(needle) |
| 510 | for i := 0; i+len(needleTokens) <= len(tokens); i++ { |
| 511 | matches := true |
| 512 | for j, token := range needleTokens { |
| 513 | if tokens[i+j] != token { |
| 514 | matches = false |
| 515 | break |
| 516 | } |
| 517 | } |
| 518 | if !matches { |
| 519 | continue |
| 520 | } |
| 521 | if deliveryMutationTokensNegated(tokens[:i]) { |
| 522 | negated = true |
| 523 | } else { |
| 524 | affirmative = true |
| 525 | } |
| 526 | } |
| 527 | return affirmative, negated |
| 528 | } |
| 529 | |
| 530 | func deliveryMutationTokensNegated(prefix []string) bool { |
| 531 | if len(prefix) > 6 { |
| 532 | prefix = prefix[len(prefix)-6:] |
| 533 | } |
| 534 | boundary := -1 |
| 535 | for i, token := range prefix { |
| 536 | switch token { |
| 537 | case "but", "however", "nevertheless", "instead", "so", "then", "therefore", "please": |
| 538 | boundary = i |
| 539 | } |
| 540 | } |
| 541 | if boundary >= 0 { |
| 542 | prefix = prefix[boundary+1:] |
| 543 | } |
| 544 | for i, token := range prefix { |
| 545 | if token == "not" && i+1 < len(prefix) && prefix[i+1] == "only" { |
| 546 | continue |
| 547 | } |
| 548 | switch token { |
| 549 | case "not", "never", "without", "cannot", "can't", "cant", "don't", "dont", "won't", "wont", "unable", "avoid", "avoiding", "afraid", "refuse", "refusing", "needn't": |
| 550 | return true |
| 551 | case "no": |
| 552 | if i+1 < len(prefix) && prefix[i+1] == "need" { |
| 553 | return true |
| 554 | } |
| 555 | } |
| 556 | } |
| 557 | return false |
| 558 | } |
| 559 | |
| 560 | func deliveryMutationRunesNegated(prefix []rune) bool { |
| 561 | if len(prefix) > 12 { |
| 562 | prefix = prefix[len(prefix)-12:] |
| 563 | } |
| 564 | window := string(prefix) |
| 565 | scopeStart := 0 |
| 566 | for _, boundary := range []string{"所以", "然后", "而是", "转而", "改为"} { |
| 567 | if index := strings.LastIndex(window, boundary); index >= 0 { |
| 568 | end := index + len(boundary) |
| 569 | if end > scopeStart { |
| 570 | scopeStart = end |
| 571 | } |
| 572 | } |
| 573 | } |
| 574 | if index := strings.LastIndex(window, "请"); index >= 0 { |
| 575 | before, after := window[:index], window[index+len("请"):] |
| 576 | requestWord := strings.HasSuffix(before, "申") || strings.HasPrefix(after, "求") |
| 577 | negatedRequest := false |
| 578 | for _, marker := range []string{"不要", "不能", "无法", "不想", "不敢", "无需", "不需要", "不可", "没法", "禁止", "拒绝"} { |
| 579 | if strings.HasSuffix(before, marker) || strings.Contains(after, marker) { |
| 580 | negatedRequest = true |
| 581 | break |
| 582 | } |
| 583 | } |
| 584 | if !requestWord && !negatedRequest && index+len("请") > scopeStart { |
| 585 | scopeStart = index + len("请") |
| 586 | } |
| 587 | } |
| 588 | window = window[scopeStart:] |
| 589 | for _, marker := range []string{"不要", "别", "勿", "不能", "无法", "不想", "不敢", "无需", "不需要", "不可", "没法", "没有", "禁止", "拒绝"} { |
| 590 | if strings.Contains(window, marker) { |
| 591 | return true |
| 592 | } |
| 593 | } |
| 594 | return false |
| 595 | } |
| 596 | |
| 597 | func containsAnySubstring(s string, terms []string) bool { |
| 598 | for _, term := range terms { |
| 599 | if strings.Contains(s, term) { |
| 600 | return true |
| 601 | } |
| 602 | } |
| 603 | return false |
| 604 | } |
| 605 |