opencrabs 0.3.77

The autonomous, self-improving AI agent. Single Rust binary. Every channel. Install with: cargo install opencrabs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
//! Phantom-tool-call detection.
//!
//! Catches assistant text that narrates actions ("Let me check…", "I'll
//! update…", "Pushed.") without emitting any actual tool calls. Two
//! detectors:
//!
//! * `has_phantom_tool_intent_no_tools` — relaxed gate, used when the
//!   iteration already produced zero tool uses. Bare intent phrases or
//!   short past-tense terminal claims are sufficient.
//! * `has_phantom_tool_intent` — strict gate for the general path; needs
//!   either standalone strong signals (multi-step plans, completion
//!   claims, gerund drops) or an intent phrase + file-path corroboration.
//!
//! All language-dependent data (intent phrases, action verbs, regex
//! patterns) lives in `phantom_lang/` TOML files, loaded at compile time.
//! Language detection is automatic via character-set heuristics.

use regex::Regex;
use std::sync::LazyLock;

use super::phantom_lang;

/// Relaxed phantom detection used when the caller already knows the
/// model emitted **zero tool_use blocks** this iteration. In that case
/// any bare intent phrase is phantom — no path or extension
/// corroboration required, because the tool count already proves
/// nothing happened.
///
/// Structured answers are exempt. Commit-log tables, code blocks, and
/// long bulleted lists inevitably contain intent-phrase substrings
/// (e.g. a commit message literally titled
/// `"fix(heal): phantom detector lets 'Let me check...' loops slide"`
/// — seen in logs 2026-04-17 03:38:37 — triggered this detector on
/// itself). A legitimate answer rendered as a table is NEVER a phantom,
/// even if its content happens to quote a phrase we watch for.
pub fn has_phantom_tool_intent_no_tools(text: &str) -> bool {
    // <<react:🔥>> and sibling inline directives prefix the narration and
    // broke every ^-anchored pattern (#464: "<<react:🔥>> Pushing the 3
    // commits now." sailed through). Strip them before any matching.
    let cleaned = strip_inline_directives(text);
    let trimmed = cleaned.trim();
    if trimmed.is_empty() {
        return false;
    }
    let lang = phantom_lang::detect_language(trimmed);
    let too_short_for_phrases = trimmed.len() < 20;

    // Both ENDS, not just the lead. Every rule used to match against the lead
    // alone, which is the first 7 lines or up to the first list item. That held
    // while a turn opened with its intent ("Let me read the config."), and
    // stopped holding once reasoning began occupying the lead: the announcement
    // slid past the window and nothing was examined but deliberation. An
    // announcement sits at one end of a turn or the other, never buried
    // mid-paragraph, so the tail closes the gap without widening the middle
    // where discussing an action would false-positive (#783).
    for window in [prose_lead_in(trimmed), prose_tail(trimmed)] {
        if window.is_empty() {
            continue;
        }
        // Brief present-continuous work announcements ("Running checks now.",
        // "Checking the logs…") are phantom on their own — the model says it's
        // acting but emitted no tool call. At 19 bytes "Running checks now."
        // falls under the length floor below, so check this before it.
        if matches_work_announcement(window) {
            return true;
        }
        // Leading-imminence announcement ("... Now downloading the fonts.") —
        // the work-announcement regex needs a trailing marker and misses it.
        if matches_now_gerund(window) {
            return true;
        }
        if too_short_for_phrases {
            continue;
        }
        let lower = window.to_lowercase();
        if lang_intent_match_any(&lower) {
            return true;
        }
        // Past-tense completion claims stay gated to the detected language:
        // action_verbs are short single words with real cross-language
        // collision risk, unlike the multi-word intent phrases above.
        if has_past_tense_action_claim(&lower, &lang.action_verbs) {
            return true;
        }
    }
    false
}

/// The turn's closing prose, mirroring [`prose_lead_in`] from the other end.
///
/// Walks backwards over at most [`TAIL_LINES`] lines and stops at the first
/// structural one, so a table or code block at the end of an answer is not
/// mistaken for the model's parting announcement.
fn prose_tail(text: &str) -> &str {
    const TAIL_LINES: usize = 5;
    let lines: Vec<&str> = text.lines().collect();
    let first_kept = lines
        .iter()
        .enumerate()
        .rev()
        .take(TAIL_LINES)
        .take_while(|(_, line)| !is_structural_line(line))
        .map(|(idx, _)| idx)
        .last();
    let Some(first_kept) = first_kept else {
        return "";
    };
    let offset: usize = lines[..first_kept].iter().map(|l| l.len() + 1).sum();
    text[offset.min(text.len())..].trim()
}

/// Detects short past-tense completion claims like `"Pushed."`, `"Deployed."`,
/// `"Migration created."` — sentences that announce an action's done without
/// having executed any tool. Only used in the zero-tool-call path; loose
/// matching elsewhere would false-positive on conversational recaps.
fn has_past_tense_action_claim(lower: &str, action_verbs: &[String]) -> bool {
    for raw_sentence in lower.split(['.', '\n', '!']) {
        let s = raw_sentence.trim();
        if s.is_empty() || s.len() > 80 {
            continue;
        }
        let words: Vec<&str> = s
            .split_whitespace()
            .take(4)
            .map(|w| w.trim_matches(|c: char| !c.is_alphanumeric()))
            .collect();
        for verb in action_verbs {
            if let Some(pos) = words.iter().position(|w| w == verb) {
                // Passive/copular description ("the config is loaded from
                // disk") is prose about state, not a completion claim —
                // only an ACTIVE claim ("Loaded.", "Both loaded properly")
                // counts. Guard on the auxiliary right before the verb.
                const AUX: &[&str] = &[
                    "is", "are", "was", "were", "been", "be", "being", "gets", "get", "got",
                    "está", "están", "foi", "é", "est", "sont",
                ];
                let passive = pos > 0 && AUX.contains(&words[pos - 1]);
                if !passive {
                    return true;
                }
            }
        }
    }
    false
}

/// Does the text contain any investigative/intent phrases?
/// Used by the phantom tool-call detector to identify when the model is
/// narrating an action it should be executing via tools.
pub fn has_investigative_intent(text: &str) -> bool {
    let lower = text.to_lowercase();
    lang_intent_match_any(&lower)
}

/// Forward-looking intent detector for the post-success path.
///
/// Behaves like `has_phantom_tool_intent_no_tools` but DROPS the
/// past-tense completion-claim branch. Used as the eligibility gate
/// for phantom self-heal AFTER a turn has already produced at least
/// one successful tool call: at that point past-tense summaries
/// (`Pushed.`, `Committed.`, `On main.`) are legitimate completion
/// acks and must not re-fire the detector — that's the whole reason
/// the post-success exemption exists. But FORWARD-looking intent
/// (`Let me dig into …`, `I'll check the …`, `Let me read the …`,
/// `need to update the …`) signals more tool calls promised and
/// dropped, which IS phantom regardless of how many tools already
/// ran this turn.
///
/// Logs 2026-06-03: a turn that ran one `git branch --show-current`
/// tool call then emitted "Good, on main. Let me dig into the delete
/// invitation endpoint, the email send path, and the invite flow to
/// find the bugs." silently ended without ever dispatching the three
/// promised investigations because the original exemption gate
/// (`phantom_eligible = tools_completed == 0`) disabled phantom
/// detection entirely for the post-tool-call portion of the turn.
///
/// Uses `prose_lead_in` so structural content (tables, code blocks,
/// bullet lists) past the lead-in doesn't contribute matches —
/// matches the host detector's own filter and keeps commit-message
/// tables from re-triggering the original false positive that
/// `e843f405` fixed.
pub fn has_forward_intent_post_success(text: &str) -> bool {
    let cleaned = strip_inline_directives(text);
    let trimmed = cleaned.trim();
    if trimmed.len() < 20 {
        return false;
    }
    let lead = prose_lead_in(trimmed);
    if lead.is_empty() {
        return false;
    }
    let lower = lead.to_lowercase();
    // A present-continuous work announcement ("Pushing the 3 commits
    // now.") is inherently FORWARD-looking — it can never be a completion
    // ack, no matter how many tools already ran this turn. The gate used
    // to check intent phrases only, so announcements after a successful
    // tool call closed turns as fake completions (#464). `matches_now_gerund`
    // adds the leading-imminence form ("... Now downloading the fonts.") that
    // the trailing-marker work-announcement regex misses.
    lang_intent_match_any(&lower) || matches_work_announcement(lead) || matches_now_gerund(lead)
}

/// Remove inline channel directives (`<<react:🔥>>`, `<<IMG:path>>`, …)
/// so anchored phantom patterns see the narration, not the marker (#464).
fn strip_inline_directives(text: &str) -> String {
    static DIRECTIVE_RE: LazyLock<Regex> =
        LazyLock::new(|| Regex::new(r"<<[^<>\n]{1,120}>>").expect("directive regex"));
    DIRECTIVE_RE.replace_all(text, "").into_owned()
}

/// Language-agnostic phantom tell (#463): the text names a REAL registered
/// tool while the turn executed zero tool calls. Models hallucinate tool
/// usage by naming the tool ("loaded via load_brain_file"), in ANY language,
/// so this catches narration the phrase lists cannot. Only multi-word
/// (underscore) tool names count: bare names like "bash" or "plan" are
/// ordinary prose words and would false-positive constantly.
pub fn mentions_registered_tool(text: &str, tool_names: &[String]) -> bool {
    let lower = text.to_lowercase();
    for name in tool_names {
        if !name.contains('_') {
            continue;
        }
        let mut from = 0;
        while let Some(rel) = lower[from..].find(name.as_str()) {
            let start = from + rel;
            let end = start + name.len();
            let before_ok = start == 0
                || !lower[..start]
                    .chars()
                    .next_back()
                    .is_some_and(|c| c.is_alphanumeric() || c == '_');
            let after_ok = end == lower.len()
                || !lower[end..]
                    .chars()
                    .next()
                    .is_some_and(|c| c.is_alphanumeric() || c == '_');
            if before_ok && after_ok {
                return true;
            }
            from = end;
        }
    }
    false
}

/// High-stakes side-effect categories whose truthful use REQUIRES a tool call:
/// a git push/tag, a release, a version bump, a CHANGELOG/file write, an
/// external post. Each inner slice is ONE category; matching any of its phrases
/// counts that category once. Phrases are completion-shaped ("pushed to origin",
/// "created and pushed"), NOT intent ("let me push") — intent is already caught
/// by the forward-looking detectors.
///
/// Unlike the prose/lead-in detectors this scans the FULL lowercased text, so a
/// claim buried in a markdown table cell (the exact shape that slipped a
/// fabricated "shipped" scoreboard past every other check, #680) is still seen.
const SIDE_EFFECT_CATEGORIES: &[&[&str]] = &[
    // Ship / release
    &[
        "shipped",
        "released",
        "release complete",
        "release is shipped",
        "release was completed",
        "release is done",
    ],
    // Git push
    &[
        "pushed to origin",
        "pushed to remote",
        "tag pushed",
        "pushed (",
        "push status",
        "git push",
    ],
    // Tag
    &[
        "tag created",
        "created and pushed",
        "tagged v",
        "tag `v",
        "created` and pushed",
    ],
    // Version bump
    &[
        "bumped to",
        "version bump",
        "version bumped",
        "cargo.toml bumped",
        "bumped cargo",
    ],
    // CHANGELOG / release-notes write
    &[
        "changelog updated",
        "changelog entry",
        "entry inserted",
        "new entry inserted",
    ],
    // External post / publish
    &[
        "posts appended",
        "posts appended to",
        "release posts",
        "published to",
        "posted to",
    ],
];

/// Number of DISTINCT high-stakes side-effect categories the text claims.
/// Counting distinct categories (not raw phrase hits) means one repeated word
/// can't inflate the score, but a release scoreboard (ship + push + tag + bump
/// + changelog + post) scores high.
pub fn count_unbacked_side_effect_claims(text: &str) -> usize {
    let lower = text.to_lowercase();
    SIDE_EFFECT_CATEGORIES
        .iter()
        .filter(|category| category.iter().any(|phrase| lower.contains(phrase)))
        .count()
}

/// Verification-by-construction phantom tell (#680): a turn that ran ZERO tool
/// calls yet claims 2+ distinct high-stakes side-effects is fabricating a
/// completion report — those actions (push, tag, release, version bump, file
/// write, external post) cannot happen without a tool call, so with none
/// executed the claims are false by construction. The caller gates this on
/// `tool_calls_completed_this_turn == 0`, so a real release turn (which DID run
/// git/bash tools) is never flagged. The 2+ threshold keeps a lone "I pushed it
/// earlier" from tripping while catching the multi-claim scoreboard shape.
pub fn claims_unbacked_side_effects(text: &str) -> bool {
    count_unbacked_side_effect_claims(text) >= 2
}

/// Image-generation hallucination tell (#747): a turn asserts it produced or
/// delivered an image/media result but carries no `<<IMG:>>`/`<<VID:>>` marker,
/// so nothing was actually sent. `generate_image` delivers via those markers,
/// so a "there it is / generated it / the edited image" about a visual result
/// with no marker is fabricating. The caller gates this on
/// `tool_calls_completed_this_turn == 0`, so a real generation turn (which ran
/// the tool and emitted a marker) is never flagged. A marker anywhere in the
/// text short-circuits to `false` — a real deliverable rode along.
///
/// Multilingual (like the text/tool-call phantom tells): the delivery phrases
/// and visual context words come from every `phantom_lang` TOML and are scanned
/// as a union, so a Spanish/French/Portuguese/Indonesian/Russian narration is
/// caught too, immune to language mis-detection.
pub fn claims_unbacked_media_result(text: &str) -> bool {
    if text.contains("<<IMG:") || text.contains("<<VID:") {
        return false;
    }
    let lower = text.to_lowercase();
    let claims_delivery = phantom_lang::all_langs()
        .iter()
        .flat_map(|l| l.media_delivery_phrases.iter())
        .any(|p| lower.contains(p.as_str()));
    let is_visual = phantom_lang::all_langs()
        .iter()
        .flat_map(|l| l.media_context_words.iter())
        .any(|w| lower.contains(w.as_str()));
    claims_delivery && is_visual
}

/// Count line-start intent phrases — `Let me <verb>`, `I'll <verb>`,
/// `Let's <verb>`, or `Now let me / Now I'll <verb>`. A high count in a
/// single iteration's text means the model is spinning in place: emitting
/// back-to-back narration instead of calling a tool.
///
/// Only line-starts (after optional whitespace / list bullet) count. Intent
/// phrases embedded mid-paragraph are normal prose, not narration spam.
pub fn count_intent_line_starts(text: &str) -> usize {
    let lang = phantom_lang::detect_language(text);
    if lang.line_start_re.is_empty() {
        return 0;
    }
    let re = Regex::new(&lang.line_start_re).unwrap_or_else(|_| {
        Regex::new(r"$^").unwrap() // never matches
    });
    re.find_iter(text).count()
}

/// Threshold above which a repeated intent line is treated as "model stuck in
/// a phantom loop".
pub const STUCK_INTENT_LOOP_THRESHOLD: usize = 3;

/// The highest number of times the SAME intent line-start appears (normalized:
/// trimmed, lowercased, whitespace collapsed). A genuine phantom loop repeats
/// the *same* line ("Let me check the file." over and over); a legitimate
/// multi-step plan ("check X… then Y… actually Z first") has many DISTINCT
/// intent lines, which must NOT be mistaken for a loop.
pub fn max_repeated_intent_line(text: &str) -> usize {
    let lang = phantom_lang::detect_language(text);
    if lang.line_start_re.is_empty() {
        return 0;
    }
    let Ok(re) = Regex::new(&lang.line_start_re) else {
        return 0;
    };
    let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
    let mut max = 0;
    for line in text.lines() {
        let trimmed = line.trim();
        if trimmed.is_empty() {
            continue;
        }
        // Only lines that START with an intent phrase count.
        if re.find(trimmed).map(|m| m.start() == 0).unwrap_or(false) {
            let norm = trimmed
                .to_lowercase()
                .split_whitespace()
                .collect::<Vec<_>>()
                .join(" ");
            let c = counts.entry(norm).or_insert(0);
            *c += 1;
            max = max.max(*c);
        }
    }
    max
}

/// Whether the text shows a genuine phantom loop: the SAME intent line repeated
/// `STUCK_INTENT_LOOP_THRESHOLD`+ times. Distinct intent lines (a varied plan)
/// are NOT a loop — that false positive used to kill legitimate planful replies.
pub fn is_stuck_in_intent_loop(text: &str) -> bool {
    max_repeated_intent_line(text) >= STUCK_INTENT_LOOP_THRESHOLD
}

/// Highest match count of `regex_of(lang)` across ALL supported languages.
/// Scanning the union (not just `detect_language`'s pick) makes phantom
/// detection immune to language mis-detection — e.g. one accented word
/// ("Activité") flipping an English monologue to French and silencing the
/// English patterns (the Kimi K3 case). The per-language regexes carry
/// language-distinctive tokens, so the union is collision-free.
fn max_lang_regex_count(
    lower: &str,
    regex_of: impl Fn(&phantom_lang::LangConfig) -> &str,
) -> usize {
    phantom_lang::all_langs()
        .iter()
        .filter_map(|lang| {
            let re = regex_of(lang);
            (!re.is_empty())
                .then(|| Regex::new(re).ok().map(|r| r.find_iter(lower).count()))
                .flatten()
        })
        .max()
        .unwrap_or(0)
}

/// Whether `regex_of(lang)` matches `text` in ANY supported language.
fn any_lang_regex_match(text: &str, regex_of: impl Fn(&phantom_lang::LangConfig) -> &str) -> bool {
    phantom_lang::all_langs().iter().any(|lang| {
        let re = regex_of(lang);
        !re.is_empty() && Regex::new(re).map(|r| r.is_match(text)).unwrap_or(false)
    })
}

pub fn has_phantom_tool_intent(text: &str) -> bool {
    let trimmed = text.trim();
    if trimmed.len() < 40 {
        return false;
    }
    let lower = trimmed.to_lowercase();

    // ── Strong signals (standalone — no corroboration needed) ─────────
    // All signals scan EVERY language (not just detect_language's pick), so a
    // mis-detected language can't disable them (#602 follow-up: one accented
    // word mis-routed an English monologue to French and slipped the detector).

    // 2+ imperative "Now <verb>" / "Let me <verb>" at line start = multi-step plan
    if max_lang_regex_count(&lower, |l| &l.now_imperative_re) >= 2 {
        return true;
    }

    // 2+ numbered steps with action verbs = narrated plan
    if max_lang_regex_count(&lower, |l| &l.numbered_steps_re) >= 2 {
        return true;
    }

    // 2+ past-tense standalone sentences = phantom completion narration
    if max_lang_regex_count(&lower, |l| &l.past_tense_standalone_re) >= 2 {
        return true;
    }

    // ── Completion claims (standalone) ────────────────────────────────
    if phantom_lang::all_langs()
        .iter()
        .any(|l| lang_completion_match(&lower, &l.completion_claims))
    {
        return true;
    }

    // ── Now + gerund status-then-action drops (standalone) ─────────────
    if any_lang_regex_match(trimmed, |l| &l.gerund_re) {
        return true;
    }

    // ── Trailing-colon intent ─────────────────────────────────────────
    if any_lang_regex_match(trimmed, |l| &l.trailing_colon_re) {
        return true;
    }

    // ── Weak signals (need corroboration) ─────────────────────────────
    // `lang_intent_match_any` already scans all languages.
    if lang_intent_match_any(&lower) {
        // Corroborate with file paths, extensions, or backtick code refs.
        // These are language-agnostic, so any language's regex works.
        let path = any_lang_regex_match(trimmed, |l| &l.path_re)
            || any_lang_regex_match(trimmed, |l| &l.ext_re)
            || any_lang_regex_match(trimmed, |l| &l.backtick_code_re);
        if path {
            return true;
        }
    }

    false
}

// ── Language-agnostic helpers ──────────────────────────────────────────

/// Check if `lower` contains any phrase from the list (case-insensitive).
fn lang_intent_match(lower: &str, phrases: &[String]) -> bool {
    phrases.iter().any(|p| lower.contains(p.as_str()))
}

/// Check if `lower` matches an intent phrase in ANY supported language.
///
/// `detect_language` only routes Cyrillic and accented-Latin text
/// reliably, so accent-free non-English narration (e.g.
/// `"Voy a usar write_file…"`) falls through to English and would slip
/// past a detected-language-only check. Intent phrases are multi-word and
/// carry language-distinctive tokens, so the cross-language union is
/// collision-free — a Spanish phrase can't match English prose and vice
/// versa. 2026-06-12.
fn lang_intent_match_any(lower: &str) -> bool {
    phantom_lang::all_langs()
        .iter()
        .any(|lang| lang_intent_match(lower, &lang.intent_phrases))
}

/// Does `lead` OPEN with a present-continuous work announcement in ANY
/// supported language ("Running checks now.", "Verificando ahora…")? Scanned
/// across all languages like the intent phrases. Each regex is anchored to the
/// message start and requires the announcement's imminence marker (now / ahora
/// / agora / maintenant / сейчас / … / trailing :) at a sentence boundary, so
/// the model leading with the announcement and then continuing ("Running fmt,
/// clippy, tests now. Then fetching…") still matches, while an ordinary
/// sentence that merely opens with a gerund ("Reading the file is
/// straightforward.") or uses "now" as an adverb ("Running it now takes a
/// minute.") does not.
pub(crate) fn matches_work_announcement(lead: &str) -> bool {
    let lead = strip_inline_directives(lead);
    let lead = lead.trim();
    phantom_lang::all_langs().iter().any(|lang| {
        !lang.work_announcement_re.is_empty()
            && Regex::new(&lang.work_announcement_re)
                .map(|re| announcement_matches_anywhere(&re, lead))
                .unwrap_or(false)
    })
}

/// Run an anchored announcement regex against every sentence start in the
/// lead, tolerating a short lead clause before the gerund. The live escapes
/// (#464) were all anchor evasions: "Internet's back, pushing now." (clause
/// prefix), "Apologies Adolfo, you're right. Pushing now." (sentence
/// prefix). Suffix slices keep the terminal imminence markers intact.
fn announcement_matches_anywhere(re: &Regex, lead: &str) -> bool {
    let mut starts: Vec<usize> = vec![0];
    let mut after_ender = false;
    for (idx, ch) in lead.char_indices() {
        if after_ender && !ch.is_whitespace() {
            starts.push(idx);
            after_ender = false;
        }
        if matches!(ch, '.' | '!' | '?' | '\n' | '') {
            after_ender = true;
        }
    }
    for &start in &starts {
        let suffix = &lead[start..];
        if re.is_match(suffix) {
            return true;
        }
        // Short lead clause before the announcement ("internet's back, ").
        let window_end = suffix
            .char_indices()
            .take_while(|(i, _)| *i <= 48)
            .last()
            .map(|(i, c)| i + c.len_utf8())
            .unwrap_or(0);
        if let Some(comma) = suffix[..window_end].find(", ")
            && re.is_match(&suffix[comma + 2..])
        {
            return true;
        }
    }
    false
}

/// Does `text` contain a "Now &lt;gerund&gt;" work announcement at a sentence
/// start in ANY supported language (`gerund_re`)? This catches the
/// leading-imminence form ("Issue #22 filed. Now downloading the fonts.") that
/// `matches_work_announcement` misses — that regex requires a TRAILING imminence
/// marker (now / … / :), so an announcement that leads with "Now" and ends on a
/// plain period slips it. Scanned across all languages like the other tells.
pub(crate) fn matches_now_gerund(text: &str) -> bool {
    let text = strip_inline_directives(text);
    phantom_lang::all_langs().iter().any(|lang| {
        !lang.gerund_re.is_empty()
            && Regex::new(&lang.gerund_re)
                .map(|re| re.is_match(&text))
                .unwrap_or(false)
    })
}

/// Check if `lower` contains any completion claim.
fn lang_completion_match(lower: &str, claims: &[String]) -> bool {
    claims.iter().any(|c| lower.contains(c.as_str()))
}

/// Slice of the text before the first code fence, markdown table row,
/// or list-item line — the "narration" portion.
/// Whether `line` is markup rather than prose: a fence, table row, bullet, or
/// numbered item. Shared so both ends of a turn agree on where prose stops.
fn is_structural_line(line: &str) -> bool {
    let t = line.trim_start();
    t.starts_with("```")
        || (t.starts_with('|') && t.contains('|'))
        || t.starts_with("- ")
        || t.starts_with("* ")
        || t.starts_with("")
        || (t.chars().next().is_some_and(|c| c.is_ascii_digit()) && t.contains(". "))
}

fn prose_lead_in(text: &str) -> &str {
    let mut byte_offset: usize = 0;
    for (idx, line) in text.lines().enumerate() {
        if is_structural_line(line) {
            return text[..byte_offset].trim_end();
        }
        if idx >= 6 {
            break;
        }
        byte_offset += line.len() + 1;
    }
    text
}

/// Does the user message contain an analysis / data-interpretation verb?
///
/// Used to detect "the user asked me to AUDIT something" vs. "the user
/// asked me to COMMIT something" so the runtime can react when a turn
/// ends with `finish_reason: stop` and ZERO text after successful tool
/// calls. For side-effect tasks (commit / push / edit / deploy), the
/// tool call IS the deliverable — empty-text completion is fine. For
/// analysis tasks, the tool fetched data the user expected the model
/// to interpret — empty-text completion is a regression we shipped via
/// the `FINISHING A TURN` directive in commit e843f405.
///
/// Matches at a word boundary so prose like "you describe this
/// pattern" does NOT trip on "describe" inside another sentence. Only
/// the leading-imperative / question form counts.
///
/// Coverage is intentionally English-only for now. Spanish / Portuguese
/// / French / Russian variants follow the same shape; this MVP catches
/// the common case and can be expanded as patterns emerge in logs.
pub fn is_analysis_intent(text: &str) -> bool {
    let lower = text.to_lowercase();
    // Strip the channel prefix if present so `[Channel: Telegram ...]\n<msg>`
    // matches on `<msg>` content, not on the bracketed wrapper.
    let body = lower.rsplit('\n').next().unwrap_or(&lower);
    // Look at the first ~200 chars only — the verb is in the request,
    // not buried in a long quote.
    let head: String = body.chars().take(200).collect();
    // Phrase patterns to match. Each entry is matched as a contained
    // substring on the head — short verbs need leading whitespace or
    // start-of-string to avoid matching inside another word
    // ("examine" should not trigger on "exam"; "audit" must not
    // trigger on "auditorium" in a quoted URL).
    let leading_word = |w: &str| -> bool {
        // Match at start or after whitespace/punct, followed by space.
        // Cheap manual scan rather than a regex — keeps this hot path
        // allocation-free for the common no-match case.
        let needle = format!(" {w} ");
        if head.starts_with(&format!("{w} ")) {
            return true;
        }
        head.contains(&needle)
    };
    const ANALYSIS_VERBS: &[&str] = &[
        "audit",
        "review",
        "compare",
        "explain",
        "summarise",
        "summarize",
        "check",
        "describe",
        "analyse",
        "analyze",
        "find",
        "look up",
        "look at",
        "what does",
        "how does",
        "why does",
        "what is",
        "what are",
        "tell me",
        "show me",
        "investigate",
        "diagnose",
    ];
    // "report" deliberately omitted — too noun-ambiguous. "the report
    // says X" and "your report failed" would false-positive the
    // analysis-nudge while no analysis was requested. `report on X`
    // is rare enough that users who want it can rephrase as "explain
    // X" or "summarise X" without losing precision.
    ANALYSIS_VERBS.iter().any(|v| leading_word(v))
}

/// Bare terminal completion tokens. A response that is ONLY one of these
/// (after stripping directives, emoji, and punctuation) is a content-free
/// completion ack: fine as a TRUE terminal after real tool work, but a phantom
/// when the turn ran zero tools and the user asked for a deliverable.
const BARE_COMPLETION_PHRASES: &[&str] = &[
    "done",
    "all done",
    "ready",
    "all ready",
    "ready to go",
    "good to go",
    "finished",
    "all finished",
    "complete",
    "completed",
    "task complete",
    "task completed",
    "all set",
    "all good",
    "taken care of",
    "handled",
    "sorted",
    "there you go",
    "here you go",
];

/// Is `text` nothing but a bare completion word/phrase — no deliverable, no
/// substantive body?
///
/// Strips inline directives, then requires the whole remaining message
/// (lowercased, emoji/punctuation flattened to spaces, whitespace collapsed)
/// to equal one of `BARE_COMPLETION_PHRASES`. A code fence disqualifies it
/// outright (that carries the deliverable), as does any real length — a genuine
/// confirmation with specifics ("Committed as 7256f6…") is longer than any bare
/// ack. #680 follow-up: a lone "Done." is 5 bytes and slips every other
/// detector's 20/40-byte floor, so it needs an explicit, floor-free check.
pub fn is_bare_completion_only(text: &str) -> bool {
    let cleaned = strip_inline_directives(text);
    if cleaned.contains("```") {
        return false;
    }
    let trimmed = cleaned.trim();
    if trimmed.chars().count() > 48 {
        return false;
    }
    let normalized: String = trimmed
        .to_lowercase()
        .chars()
        .map(|c| if c.is_alphanumeric() { c } else { ' ' })
        .collect::<String>()
        .split_whitespace()
        .collect::<Vec<_>>()
        .join(" ");
    BARE_COMPLETION_PHRASES.contains(&normalized.as_str())
}

/// Did the user ask the model to PRODUCE an artifact — build/create/write/
/// generate code, a script, a component, a document?
///
/// Mirror of `is_analysis_intent`, used to gate the bare-completion phantom
/// check: a delivery request answered by a content-free "Done." with zero tool
/// calls produced nothing. Interrogative openers ("did you build it?") are
/// questions ABOUT work, not requests to produce it, so they never count — that
/// guard keeps a legitimate cross-turn ack from being flagged. English-only for
/// now, like `is_analysis_intent`.
pub fn is_delivery_intent(text: &str) -> bool {
    let lower = text.to_lowercase();
    // Drop a leading channel/context bracket ("[channel: telegram …]") so the
    // request itself is what we scan, not the wrapper.
    let body = lower.trim_start();
    let body = if body.starts_with('[') {
        body.split_once(']')
            .map(|x| x.1)
            .unwrap_or(body)
            .trim_start()
    } else {
        body
    };
    const QUESTION_OPENERS: &[&str] = &[
        "did ", "do ", "does ", "is ", "are ", "was ", "were ", "have ", "has ", "had ", "can ",
        "could ", "will ", "would ", "should ", "what ", "where ", "when ", "why ", "how ",
        "which ", "who ",
    ];
    if QUESTION_OPENERS.iter().any(|q| body.starts_with(q)) {
        return false;
    }
    // Build requests span multiple lines; scan the whole request but cap the
    // window so a long pasted blob can't dominate this hot path.
    let head: String = body.chars().take(1000).collect();
    let leading_word = |w: &str| -> bool {
        let needle = format!(" {w} ");
        head.starts_with(&format!("{w} ")) || head.contains(&needle)
    };
    const DELIVERY_VERBS: &[&str] = &[
        "create",
        "build",
        "generate",
        "write",
        "implement",
        "produce",
        "make",
        "draft",
        "compose",
        "design",
        "provide",
    ];
    DELIVERY_VERBS.iter().any(|v| leading_word(v))
}

/// Heuristic: does `text` look like it was truncated mid-sentence?
pub fn looks_truncated_mid_sentence(text: &str) -> bool {
    let trimmed = text.trim_end();
    if trimmed.chars().count() < 40 {
        return false;
    }
    if trimmed.ends_with("```") {
        return false;
    }
    if trimmed.ends_with('|') {
        return false;
    }
    if ends_with_url(trimmed) {
        return false;
    }
    let last = match trimmed.chars().next_back() {
        Some(c) => c,
        None => return false,
    };
    if last.is_alphanumeric() {
        return true;
    }
    matches!(
        last,
        ',' | ';' | ':' | '-' | '(' | '[' | '{' | '<' | '/' | '\\' | '&' | '@' | '#'
    )
}

/// Detect whether `text` ends with a URL.
fn ends_with_url(text: &str) -> bool {
    let trimmed = text.trim_end();
    let boundary = trimmed
        .rfind(|c: char| c.is_whitespace() || matches!(c, '(' | '[' | '{' | '<' | '"' | '\''))
        .map(|i| i + 1)
        .unwrap_or(0);
    let tail = &trimmed[boundary..];
    tail.contains("://")
}

/// Whether `text` presents literal command output that no tool produced this
/// turn.
///
/// Verb lists cannot catch this one. After a genuine grep, "Grepped and found
/// 34 hits" is an honest recap, and the sentence is identical when the grep
/// never ran — what differs is whether the CONTENT came from a tool. So this
/// checks content, not wording, and holds regardless of how many calls the
/// turn made.
///
/// The trigger is quoted evidence: grep-style `149:pub struct Tui {` lines and
/// `=== marker ===` blocks, the shapes a model uses when presenting tool
/// output. Prose and paraphrase carry neither, so ordinary answers are never
/// examined. At least [`MIN_EVIDENCE_LINES`] are required, so a passing mention
/// of a line number is not enough, and it only fires when a MAJORITY are absent
/// from every tool result — a partially-quoted real output stays clean.
///
/// This is the case where one trivial call bought immunity for a whole turn:
/// self-heal forced a tool, the model ran an `echo`, then reported the output
/// of two greps it never made (#785).
pub fn claims_unbacked_evidence(text: &str, tool_outputs: &[String]) -> bool {
    /// Below this, a numbered line is more likely a citation than a dump.
    const MIN_EVIDENCE_LINES: usize = 3;

    let evidence: Vec<&str> = text
        .lines()
        .map(str::trim)
        .filter(|l| is_evidence_line(l))
        .collect();
    if evidence.len() < MIN_EVIDENCE_LINES {
        return false;
    }
    let backed = evidence
        .iter()
        .filter(|line| tool_outputs.iter().any(|out| out.contains(**line)))
        .count();
    backed * 2 < evidence.len()
}

/// A line shaped like quoted tool output rather than prose.
///
/// Covers the three shapes fabrications have actually taken, not just the
/// first one seen: grep output, aligned key-value blocks, and column rows.
/// Built too narrowly the first time, it matched only grep lines and missed
/// two later fabrications that used the other two (#789).
fn is_evidence_line(line: &str) -> bool {
    // `149:pub struct Tui {` — grep -n, and the `wc -l` / `ls` numeric forms.
    let numbered = line.split_once(':').is_some_and(|(n, rest)| {
        !n.is_empty() && n.chars().all(|c| c.is_ascii_digit()) && !rest.trim().is_empty()
    });
    // `=== LINE COUNT ===`, the framing a model adds around a dump.
    let marker = line.len() > 6 && line.starts_with("===") && line.ends_with("===");
    // `Title  : Non-owner can no longer …` — an aligned key-value block. The
    // padding before the colon is what distinguishes it from ordinary prose
    // that happens to contain one.
    let aligned_kv = line.split_once(':').is_some_and(|(k, v)| {
        let key = k.trim_end();
        !key.is_empty()
            && k.len() > key.len()
            && key.len() <= 24
            && !key.contains(' ')
            && !v.trim().is_empty()
    });
    // `#776  TUI: pasted multi-line text …  bug, tui` — a column row: an id
    // token followed by run-together spacing.
    let column_row = line.starts_with('#')
        && line.contains("  ")
        && line[1..].chars().take_while(|c| c.is_ascii_digit()).count() >= 2;
    numbered || marker || aligned_kv || column_row
}

/// Commands the text claims to have ALREADY RUN that no tool call actually
/// contained this turn.
///
/// Every other detector here matches how a fabrication looks — its verbs, its
/// layout, where in the turn it sits — and a model can always look different.
/// This one does not infer: the loop knows exactly what it executed, so a
/// claim naming a specific command is checkable against fact.
///
/// Two conditions, both required, so a PROPOSED command stays untouched:
/// the command appears in backticks, and the surrounding sentence frames it as
/// already executed. "I could run `gh issue list`" is a suggestion; "Ran `gh
/// issue list` for real this turn" is a claim, and if no tool input contains
/// it, it is a false one (#789).
pub fn claims_uncalled_commands(text: &str, executed_inputs: &[String]) -> Vec<String> {
    let mut out = Vec::new();
    // Framings are scanned across EVERY language, like the intent phrases, not
    // taken from the detected one. The command is language-neutral but the
    // claim around it is not, and detection is a character-set heuristic that
    // reads accented Latin as the wrong language — Portuguese and Spanish
    // claims slipped through when this trusted it. Multi-word framings carry
    // little cross-language collision risk, which is why the intent phrases
    // are scanned the same way.
    for sentence in text.split(['.', '\n', '!', '?']) {
        if !frames_as_executed_any(&sentence.to_lowercase()) {
            continue;
        }
        for cmd in backticked_commands(sentence) {
            // Match on the distinctive head (program + first argument) rather
            // than the whole span: a real call may add flags, redirects or a
            // pipeline the prose omits, and that is not a fabrication.
            let head: String = cmd.split_whitespace().take(2).collect::<Vec<_>>().join(" ");
            if head.is_empty() {
                continue;
            }
            if !executed_inputs.iter().any(|inp| inp.contains(&head)) {
                out.push(cmd);
            }
        }
    }
    out
}

/// Whether the sentence presents its command as done rather than proposed.
///
/// Scanned across every language's `executed_framings`, so a claim in
/// Portuguese or Russian is caught the same as one in English.
fn frames_as_executed_any(lower: &str) -> bool {
    phantom_lang::all_langs().iter().any(|lang| {
        lang.executed_framings
            .iter()
            .any(|m| lower.contains(m.as_str()))
    })
}

/// Backticked spans that look like a shell command: a bare program name
/// followed by at least one argument. Prose in backticks (`mod.rs`, a symbol,
/// a file path) has no space and is skipped.
fn backticked_commands(text: &str) -> Vec<String> {
    const PROGRAMS: &[&str] = &[
        "gh",
        "git",
        "cargo",
        "npm",
        "pnpm",
        "yarn",
        "grep",
        "rg",
        "ls",
        "cat",
        "wc",
        "sed",
        "awk",
        "curl",
        "docker",
        "psql",
        "sqlite3",
        "python",
        "python3",
        "make",
        "find",
        "head",
        "tail",
        "shasum",
        "sha256sum",
        "diff",
        "kubectl",
        "terraform",
    ];
    let mut out = Vec::new();
    let mut rest = text;
    while let Some(open) = rest.find('`') {
        let after = &rest[open + 1..];
        let Some(close) = after.find('`') else { break };
        let span = after[..close].trim();
        rest = &after[close + 1..];
        let mut words = span.split_whitespace();
        if let Some(prog) = words.next()
            && words.next().is_some()
            && PROGRAMS.contains(&prog)
        {
            out.push(span.to_string());
        }
    }
    out
}

/// Did every tool call this turn do nothing (#825)?
///
/// The post-success exemption asks "did any tool succeed", when what actually
/// vouches for a claim is "did a tool DO anything". A model that cannot or
/// will not make a real call can satisfy the former indefinitely with `true`.
///
/// Observed: seven green tool calls, none of them the one being claimed. Two
/// `true` no-ops, two `echo`s narrating an intent (`echo "attempting
/// telegram_send"`), and the turn then asserted it had "tried telegram_send a
/// dozen times". The calls were the substitute for the work, and they bought
/// immunity from every check that keys off tool success.
///
/// Returns false for an empty turn: no calls at all is already handled by the
/// zero-call branch, and reporting it here would double-count.
pub fn all_calls_were_null_effect(tool_inputs: &[String]) -> bool {
    !tool_inputs.is_empty() && tool_inputs.iter().all(|input| is_null_effect_call(input))
}

/// Does this single call produce no state change and no observation?
///
/// Only shell commands can be null: a `read_file` or `grep` observes
/// something by definition, so anything without a `command` field counts as
/// real work.
fn is_null_effect_call(input_json: &str) -> bool {
    let Some(command) = extract_command_field(input_json) else {
        return false;
    };
    is_null_effect_command(&command)
}

/// `true`, `:`, or a bare `echo` of a literal.
///
/// Deliberately narrow. `echo` legitimately writes files and builds
/// here-documents, and its output is often consumed downstream, so anything
/// carrying a redirect, pipe, or chain is real work. What is null is `echo`
/// whose entire effect is printing a string nobody reads.
pub fn is_null_effect_command(command: &str) -> bool {
    let trimmed = command.trim();
    if trimmed.is_empty() {
        return true;
    }
    // A chain does something even if one link is trivial.
    if trimmed.contains("&&")
        || trimmed.contains("||")
        || trimmed.contains('|')
        || trimmed.contains('>')
        || trimmed.contains('<')
        || trimmed.contains(';')
        || trimmed.contains('`')
        || trimmed.contains("$(")
    {
        return false;
    }
    if trimmed == "true" || trimmed == ":" {
        return true;
    }
    trimmed.strip_prefix("echo ").is_some_and(|rest| {
        // `echo $VAR` reads state; a quoted or bare literal does not.
        !rest.contains('$')
    })
}

/// Pull `command` out of a tool-input JSON blob.
fn extract_command_field(input_json: &str) -> Option<String> {
    let value: serde_json::Value = serde_json::from_str(input_json).ok()?;
    value
        .get("command")
        .and_then(|v| v.as_str())
        .map(str::to_string)
}

/// Tool-input fragments that mean a file was actually handed to a surface.
///
/// Checked as substrings of the raw input JSON so a new channel's send does
/// not silently fall outside the check.
///
/// Deliberately underscore-style action names and parameter keys only. A first
/// attempt included bare words like `attachment`, and the very turn this
/// detector exists for defeated it: its `tool_search` query was "send telegram
/// document file attachment", so SEARCHING for how to send a file counted as
/// having sent one. Prose never contains `send_document` or `document_url`.
const FILE_SEND_MARKERS: &[&str] = &[
    "send_document",
    "send_file",
    "upload_file",
    "document_url",
    "file_url",
    "media_url",
];

/// Claimed a file was delivered when nothing sent one (#825).
///
/// The image equivalent (#747) checks for an `<<IMG:>>` marker in the text,
/// because `generate_image` delivers inline. A document goes out through a
/// tool call instead, so the evidence lives in what the turn INVOKED, not in
/// what it wrote. Document sending arrived after those media checks, which is
/// why this shape was uncovered.
///
/// Observed: a turn ran `write_file`, four no-ops, and `tool_search`, never
/// invoked any send, and opened its reply with "File sent above." The file
/// existed on disk; nothing had carried it to the chat.
///
/// Multilingual by construction — phrases come from every `phantom_lang`
/// TOML scanned as a union, never via `detect_language`, so an accented
/// language cannot disable it.
pub fn claims_unsent_file(text: &str, tool_inputs: &[String]) -> bool {
    // Something really did ship a file: nothing to flag.
    if tool_inputs
        .iter()
        .any(|input| FILE_SEND_MARKERS.iter().any(|m| input.contains(m)))
    {
        return false;
    }
    let lower = text.to_lowercase();
    let claims_delivery = phantom_lang::all_langs()
        .iter()
        .flat_map(|l| l.file_delivery_phrases.iter())
        .any(|p| lower.contains(p.as_str()));
    if !claims_delivery {
        return false;
    }
    // The phrase must actually be about a file. "sent above" alone is said of
    // ordinary messages all the time.
    phantom_lang::all_langs()
        .iter()
        .flat_map(|l| l.file_context_words.iter())
        .any(|w| lower.contains(w.as_str()))
}