@oh-my-pi/omp-stats 16.3.8 → 16.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/client/index.js +1 -1
- package/dist/types/types.d.ts +2 -2
- package/dist/types/user-metrics.d.ts +14 -5
- package/package.json +4 -4
- package/src/client/routes/BehaviorRoute.tsx +3 -3
- package/src/db.ts +23 -1
- package/src/types.ts +2 -2
- package/src/user-metrics.ts +78 -68
package/dist/types/types.d.ts
CHANGED
|
@@ -99,13 +99,13 @@ export interface UserMessageStats {
|
|
|
99
99
|
yelling: number;
|
|
100
100
|
/** Profanity hits */
|
|
101
101
|
profanity: number;
|
|
102
|
-
/** Catch-all upset signal: drama runs + `noooo`/`ughh`/... + `dude` +
|
|
102
|
+
/** Catch-all upset signal: drama runs + `noooo`/`ughh`/... + `dude` + `:(` */
|
|
103
103
|
anguish: number;
|
|
104
104
|
/** Corrective negation ("no", "nope", "thats not what i meant") */
|
|
105
105
|
negation: number;
|
|
106
106
|
/** User repeating themselves ("i meant", "still doesnt work", "like i said") */
|
|
107
107
|
repetition: number;
|
|
108
|
-
/** Second-person reproach ("you didnt", "you
|
|
108
|
+
/** Second-person reproach ("you didnt", "why did you", "stop X-ing") */
|
|
109
109
|
blame: number;
|
|
110
110
|
}
|
|
111
111
|
/**
|
|
@@ -13,6 +13,9 @@ export interface UserMessageMetrics {
|
|
|
13
13
|
* Number of "yelling" sentences: sentences where more than half of the
|
|
14
14
|
* alphabetic characters are uppercase (and there are enough letters to
|
|
15
15
|
* make the ratio meaningful - short acronyms like "OK" don't count).
|
|
16
|
+
* A sentence also needs either two uppercase runs ("WHAT THE HELL") or
|
|
17
|
+
* one elongated run ("CMOOON") so a lone acronym/env var ("use JSON",
|
|
18
|
+
* "HOME=/tmp") doesn't register.
|
|
16
19
|
*/
|
|
17
20
|
yelling: number;
|
|
18
21
|
/** Profanity hits (word-boundary, case-insensitive). */
|
|
@@ -25,11 +28,11 @@ export interface UserMessageMetrics {
|
|
|
25
28
|
*
|
|
26
29
|
* Sum of:
|
|
27
30
|
* - drama runs: 3+ `!` / `?` (with `1`-mishit fallout)
|
|
28
|
-
* - elongated
|
|
29
|
-
* `
|
|
30
|
-
* `
|
|
31
|
+
* - interjections, elongated where the short form is ambiguous:
|
|
32
|
+
* `noooo`, `ahhh`, `ugh(h)`, `argh`, `grr`, `stooop`, `whyyy`,
|
|
33
|
+
* `fuuu(ck)`, `wtfff`, `omggg`, `yesss`, `goddd`, `bruhh`
|
|
31
34
|
* - standalone `dude`
|
|
32
|
-
* -
|
|
35
|
+
* - sad emoticons: `:(`, `;(`, `:-(((`
|
|
33
36
|
*/
|
|
34
37
|
anguish: number;
|
|
35
38
|
/**
|
|
@@ -37,9 +40,14 @@ export interface UserMessageMetrics {
|
|
|
37
40
|
*
|
|
38
41
|
* Counted on the same prose-only body as {@link anguish}.
|
|
39
42
|
*
|
|
40
|
-
* - line-leading `
|
|
43
|
+
* - line-leading `nope` / `nah` / `nvm` / `wrong` / `incorrect`
|
|
41
44
|
* (word-bounded, so `now`, `nobody`, `north` don't match)
|
|
45
|
+
* - line-leading `no` when used as an interjection - followed by
|
|
46
|
+
* punctuation, end-of-text, or a discourse word (`no i meant`,
|
|
47
|
+
* `no, wait`). Determiner `no` (`no extensions to the page`,
|
|
48
|
+
* `no auto start`) doesn't count.
|
|
42
49
|
* - `that(?:'s)? not (what|right|it)` and `not what i (meant|asked|said|wanted)`
|
|
50
|
+
* - `makes (no|zero) sense`
|
|
43
51
|
*/
|
|
44
52
|
negation: number;
|
|
45
53
|
/**
|
|
@@ -58,6 +66,7 @@ export interface UserMessageMetrics {
|
|
|
58
66
|
* Direct second-person reproach pinned on the agent:
|
|
59
67
|
*
|
|
60
68
|
* - `you (didn't|did not|broke|missed|forgot|keep|always|never|still|ignored)`
|
|
69
|
+
* - `why (would|did) (you|u)`
|
|
61
70
|
* - sentence-leading `stop <verb>ing` imperatives
|
|
62
71
|
*/
|
|
63
72
|
blame: number;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/omp-stats",
|
|
4
|
-
"version": "16.3.
|
|
4
|
+
"version": "16.3.10",
|
|
5
5
|
"description": "Local observability dashboard for pi AI usage statistics",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"fmt": "biome format --write ."
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-ai": "16.3.
|
|
43
|
-
"@oh-my-pi/pi-catalog": "16.3.
|
|
44
|
-
"@oh-my-pi/pi-utils": "16.3.
|
|
42
|
+
"@oh-my-pi/pi-ai": "16.3.10",
|
|
43
|
+
"@oh-my-pi/pi-catalog": "16.3.10",
|
|
44
|
+
"@oh-my-pi/pi-utils": "16.3.10",
|
|
45
45
|
"@tailwindcss/node": "^4.3.0",
|
|
46
46
|
"chart.js": "^4.5.1",
|
|
47
47
|
"date-fns": "^4.4.0",
|
|
@@ -132,14 +132,14 @@ function BehaviorSummaryPanel({
|
|
|
132
132
|
const METRIC_OPTIONS = [
|
|
133
133
|
{ value: "yelling", label: "CAPS", title: "Yelling (CAPS)" },
|
|
134
134
|
{ value: "profanity", label: "Profanity", title: "Profanity" },
|
|
135
|
-
{ value: "anguish", label: "Anguish", title: "Anguish (!!!, nooo, dude,
|
|
136
|
-
{ value: "negation", label: "Negation", title: "Negation (no/nope/wrong)" },
|
|
135
|
+
{ value: "anguish", label: "Anguish", title: "Anguish (!!!, nooo, ugh, dude, ':(')" },
|
|
136
|
+
{ value: "negation", label: "Negation", title: "Negation (no/nope/wrong, makes no sense)" },
|
|
137
137
|
{
|
|
138
138
|
value: "repetition",
|
|
139
139
|
label: "Repetition",
|
|
140
140
|
title: "Repetition (i meant, still doesnt)",
|
|
141
141
|
},
|
|
142
|
-
{ value: "blame", label: "Blame", title: "Blame (you didnt, stop X-ing)" },
|
|
142
|
+
{ value: "blame", label: "Blame", title: "Blame (you didnt, why did you, stop X-ing)" },
|
|
143
143
|
{
|
|
144
144
|
value: "frustration",
|
|
145
145
|
label: "Frustration",
|
package/src/db.ts
CHANGED
|
@@ -46,7 +46,7 @@ let db: Database | null = null;
|
|
|
46
46
|
|
|
47
47
|
const BACKFILL_COMPLETE = "complete";
|
|
48
48
|
const BACKFILL_PENDING = "pending";
|
|
49
|
-
const USER_MESSAGES_BACKFILL_KEY = "
|
|
49
|
+
const USER_MESSAGES_BACKFILL_KEY = "user_messages_v8";
|
|
50
50
|
const USER_MESSAGE_LINKS_REPAIR_KEY = "user_message_links_v1";
|
|
51
51
|
const PRIORITY_PREMIUM_REQUESTS_BACKFILL_KEY = "premium_requests_priority_v1";
|
|
52
52
|
const AGENT_TYPE_BACKFILL_KEY = "agent_type_v1";
|
|
@@ -206,6 +206,13 @@ export async function initDb(): Promise<Database> {
|
|
|
206
206
|
// plus profanity dictionary expansion + word-boundary fix.
|
|
207
207
|
// v4 -> v5: column `yelling_sentences` renamed to `yelling` to match
|
|
208
208
|
// the other single-word signal columns.
|
|
209
|
+
// v5 -> v6: dropped `git` from the profanity word list.
|
|
210
|
+
// v6 -> v7: dropped dot runs from `anguish`, technical-collision and
|
|
211
|
+
// opinion words from the profanity list; gated yelling on
|
|
212
|
+
// multi-word caps and bare `no` on interjection use.
|
|
213
|
+
// v7 -> v8: `no-op` compounds no longer count as negation; recovered
|
|
214
|
+
// measured false negatives: `:(` emoticons -> anguish,
|
|
215
|
+
// `why (would|did) you` -> blame, `makes no sense` -> negation.
|
|
209
216
|
const userMessageColumns = db.prepare("PRAGMA table_info(user_messages)").all() as {
|
|
210
217
|
name: string;
|
|
211
218
|
}[];
|
|
@@ -893,6 +900,21 @@ export function getCostTimeSeries(days = 90, cutoff?: number | null): CostTimeSe
|
|
|
893
900
|
* single-word signal columns (profanity, anguish, negation, ...).
|
|
894
901
|
* - v6: dropped `git` from the profanity word list - it collided with the
|
|
895
902
|
* version-control tool name, so existing rows over-counted profanity.
|
|
903
|
+
* - v7: false-positive trim measured against the real corpus: dot runs
|
|
904
|
+
* (`..`/`...`) no longer count as anguish; profanity list dropped
|
|
905
|
+
* technical-collision words (`dummy`, `blast`, `knob`, `trash`, `crud`,
|
|
906
|
+
* `garbage`, ...), opinion/dislike words (`useless`, `awful`, `hate`,
|
|
907
|
+
* `meh`, ...) and moved `ugh`/`argh`/`grr` interjections to anguish;
|
|
908
|
+
* yelling now requires multi-word caps (filenames like `AGENTS.md` no
|
|
909
|
+
* longer fragment into all-caps sentences); bare leading `no` only
|
|
910
|
+
* counts as negation when used as an interjection, not a determiner.
|
|
911
|
+
* - v8: `no-op`-style compounds no longer count as corrective negation
|
|
912
|
+
* (hyphen after bare `no` only counts as a separator when it isn't
|
|
913
|
+
* gluing a compound word), and three measured false-negative clusters
|
|
914
|
+
* were recovered: sad emoticons (`:(`) score anguish, `why (would|did)
|
|
915
|
+
* you` scores blame, `makes (no|zero) sense` scores negation. v7
|
|
916
|
+
* shipped briefly without these, so any database that completed the v7
|
|
917
|
+
* backfill needs one more re-derive.
|
|
896
918
|
*
|
|
897
919
|
* Existing `messages` rows are unaffected - `INSERT OR IGNORE` keeps them.
|
|
898
920
|
*/
|
package/src/types.ts
CHANGED
|
@@ -103,13 +103,13 @@ export interface UserMessageStats {
|
|
|
103
103
|
yelling: number;
|
|
104
104
|
/** Profanity hits */
|
|
105
105
|
profanity: number;
|
|
106
|
-
/** Catch-all upset signal: drama runs + `noooo`/`ughh`/... + `dude` +
|
|
106
|
+
/** Catch-all upset signal: drama runs + `noooo`/`ughh`/... + `dude` + `:(` */
|
|
107
107
|
anguish: number;
|
|
108
108
|
/** Corrective negation ("no", "nope", "thats not what i meant") */
|
|
109
109
|
negation: number;
|
|
110
110
|
/** User repeating themselves ("i meant", "still doesnt work", "like i said") */
|
|
111
111
|
repetition: number;
|
|
112
|
-
/** Second-person reproach ("you didnt", "you
|
|
112
|
+
/** Second-person reproach ("you didnt", "why did you", "stop X-ing") */
|
|
113
113
|
blame: number;
|
|
114
114
|
}
|
|
115
115
|
|
package/src/user-metrics.ts
CHANGED
|
@@ -14,6 +14,9 @@ export interface UserMessageMetrics {
|
|
|
14
14
|
* Number of "yelling" sentences: sentences where more than half of the
|
|
15
15
|
* alphabetic characters are uppercase (and there are enough letters to
|
|
16
16
|
* make the ratio meaningful - short acronyms like "OK" don't count).
|
|
17
|
+
* A sentence also needs either two uppercase runs ("WHAT THE HELL") or
|
|
18
|
+
* one elongated run ("CMOOON") so a lone acronym/env var ("use JSON",
|
|
19
|
+
* "HOME=/tmp") doesn't register.
|
|
17
20
|
*/
|
|
18
21
|
yelling: number;
|
|
19
22
|
/** Profanity hits (word-boundary, case-insensitive). */
|
|
@@ -26,11 +29,11 @@ export interface UserMessageMetrics {
|
|
|
26
29
|
*
|
|
27
30
|
* Sum of:
|
|
28
31
|
* - drama runs: 3+ `!` / `?` (with `1`-mishit fallout)
|
|
29
|
-
* - elongated
|
|
30
|
-
* `
|
|
31
|
-
* `
|
|
32
|
+
* - interjections, elongated where the short form is ambiguous:
|
|
33
|
+
* `noooo`, `ahhh`, `ugh(h)`, `argh`, `grr`, `stooop`, `whyyy`,
|
|
34
|
+
* `fuuu(ck)`, `wtfff`, `omggg`, `yesss`, `goddd`, `bruhh`
|
|
32
35
|
* - standalone `dude`
|
|
33
|
-
* -
|
|
36
|
+
* - sad emoticons: `:(`, `;(`, `:-(((`
|
|
34
37
|
*/
|
|
35
38
|
anguish: number;
|
|
36
39
|
/**
|
|
@@ -38,9 +41,14 @@ export interface UserMessageMetrics {
|
|
|
38
41
|
*
|
|
39
42
|
* Counted on the same prose-only body as {@link anguish}.
|
|
40
43
|
*
|
|
41
|
-
* - line-leading `
|
|
44
|
+
* - line-leading `nope` / `nah` / `nvm` / `wrong` / `incorrect`
|
|
42
45
|
* (word-bounded, so `now`, `nobody`, `north` don't match)
|
|
46
|
+
* - line-leading `no` when used as an interjection - followed by
|
|
47
|
+
* punctuation, end-of-text, or a discourse word (`no i meant`,
|
|
48
|
+
* `no, wait`). Determiner `no` (`no extensions to the page`,
|
|
49
|
+
* `no auto start`) doesn't count.
|
|
43
50
|
* - `that(?:'s)? not (what|right|it)` and `not what i (meant|asked|said|wanted)`
|
|
51
|
+
* - `makes (no|zero) sense`
|
|
44
52
|
*/
|
|
45
53
|
negation: number;
|
|
46
54
|
/**
|
|
@@ -59,6 +67,7 @@ export interface UserMessageMetrics {
|
|
|
59
67
|
* Direct second-person reproach pinned on the agent:
|
|
60
68
|
*
|
|
61
69
|
* - `you (didn't|did not|broke|missed|forgot|keep|always|never|still|ignored)`
|
|
70
|
+
* - `why (would|did) (you|u)`
|
|
62
71
|
* - sentence-leading `stop <verb>ing` imperatives
|
|
63
72
|
*/
|
|
64
73
|
blame: number;
|
|
@@ -69,9 +78,13 @@ export interface UserMessageMetrics {
|
|
|
69
78
|
*
|
|
70
79
|
* Broad English coverage: f-/s-word families and their censored variants,
|
|
71
80
|
* mild swears, intelligence-based insults, body-part insults, British/
|
|
72
|
-
* Australian/Irish slang, religious exclamations, chat acronyms
|
|
73
|
-
*
|
|
74
|
-
*
|
|
81
|
+
* Australian/Irish slang, religious exclamations, and chat acronyms.
|
|
82
|
+
* Curated to exclude racial, homophobic, and other identity slurs, and
|
|
83
|
+
* words whose dominant use in a coding corpus is technical rather than
|
|
84
|
+
* profane (`dummy` data, `blast` radius, config `knob`, `trash` bin,
|
|
85
|
+
* CRUD, `garbage` files) or plain opinion (`useless`, `awful`, `meh`,
|
|
86
|
+
* `hate`). Pure frustration interjections (`ugh`, `argh`, `grr`) are
|
|
87
|
+
* scored as anguish instead.
|
|
75
88
|
*/
|
|
76
89
|
const PROFANITY: readonly string[] = [
|
|
77
90
|
// f-word family
|
|
@@ -183,8 +196,6 @@ const PROFANITY: readonly string[] = [
|
|
|
183
196
|
"hecks",
|
|
184
197
|
"heckin",
|
|
185
198
|
"gosh",
|
|
186
|
-
"blast",
|
|
187
|
-
"blasted",
|
|
188
199
|
"bloody",
|
|
189
200
|
"bollocks",
|
|
190
201
|
"bollox",
|
|
@@ -197,7 +208,6 @@ const PROFANITY: readonly string[] = [
|
|
|
197
208
|
"crapped",
|
|
198
209
|
"crapping",
|
|
199
210
|
"crapload",
|
|
200
|
-
"crapshoot",
|
|
201
211
|
"crapola",
|
|
202
212
|
// piss family
|
|
203
213
|
"piss",
|
|
@@ -281,7 +291,6 @@ const PROFANITY: readonly string[] = [
|
|
|
281
291
|
"cockblock",
|
|
282
292
|
"cocksucker",
|
|
283
293
|
"cocksuckers",
|
|
284
|
-
"knob",
|
|
285
294
|
"knobhead",
|
|
286
295
|
"knobheads",
|
|
287
296
|
"knobend",
|
|
@@ -327,8 +336,6 @@ const PROFANITY: readonly string[] = [
|
|
|
327
336
|
"dumber",
|
|
328
337
|
"dumbest",
|
|
329
338
|
"dumbo",
|
|
330
|
-
"dummy",
|
|
331
|
-
"dummies",
|
|
332
339
|
"fool",
|
|
333
340
|
"fools",
|
|
334
341
|
"foolish",
|
|
@@ -387,31 +394,13 @@ const PROFANITY: readonly string[] = [
|
|
|
387
394
|
"bugger",
|
|
388
395
|
"buggered",
|
|
389
396
|
// generic aggression / dismissal
|
|
390
|
-
"hate",
|
|
391
|
-
"hated",
|
|
392
|
-
"hates",
|
|
393
|
-
"hating",
|
|
394
|
-
"hateful",
|
|
395
397
|
"suck",
|
|
396
398
|
"sucks",
|
|
397
399
|
"sucked",
|
|
398
400
|
"sucking",
|
|
399
401
|
"sucky",
|
|
400
402
|
"suckage",
|
|
401
|
-
"trash",
|
|
402
403
|
"trashy",
|
|
403
|
-
"trashed",
|
|
404
|
-
"garbage",
|
|
405
|
-
"crud",
|
|
406
|
-
"crudded",
|
|
407
|
-
// quality-dismissal ("this is garbage / pointless")
|
|
408
|
-
"useless",
|
|
409
|
-
"pointless",
|
|
410
|
-
"horrible",
|
|
411
|
-
"awful",
|
|
412
|
-
"worthless",
|
|
413
|
-
"ridiculous",
|
|
414
|
-
"nonsense",
|
|
415
404
|
// religious exclamations
|
|
416
405
|
"jesus",
|
|
417
406
|
"christ",
|
|
@@ -439,26 +428,6 @@ const PROFANITY: readonly string[] = [
|
|
|
439
428
|
"lmfao",
|
|
440
429
|
"fubar",
|
|
441
430
|
"snafu",
|
|
442
|
-
// frustration interjections
|
|
443
|
-
"ugh",
|
|
444
|
-
"ughh",
|
|
445
|
-
"ughhh",
|
|
446
|
-
"urgh",
|
|
447
|
-
"argh",
|
|
448
|
-
"arghh",
|
|
449
|
-
"arghhh",
|
|
450
|
-
"arrgh",
|
|
451
|
-
"blah",
|
|
452
|
-
"bleh",
|
|
453
|
-
"meh",
|
|
454
|
-
"yikes",
|
|
455
|
-
"yeesh",
|
|
456
|
-
"oof",
|
|
457
|
-
"gah",
|
|
458
|
-
"gahh",
|
|
459
|
-
"grr",
|
|
460
|
-
"grrr",
|
|
461
|
-
"grrrr",
|
|
462
431
|
];
|
|
463
432
|
|
|
464
433
|
const PROFANITY_RE = new RegExp(String.raw`\b(?:${PROFANITY.join("|")})\b`, "gi");
|
|
@@ -473,16 +442,18 @@ const YELLING_THRESHOLD = 0.5;
|
|
|
473
442
|
const DRAMA_RE = /[!?][!?1]{2,}/g;
|
|
474
443
|
const WORD_RE = /\S+/g;
|
|
475
444
|
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
// (
|
|
479
|
-
//
|
|
480
|
-
//
|
|
445
|
+
// Anguish/exasperation interjections. Each alternative is a case-insensitive
|
|
446
|
+
// word-bounded pattern. Interjections whose short form collides with normal
|
|
447
|
+
// prose ("no", "ahh", "why", "yes", "god") require *real* elongation;
|
|
448
|
+
// unambiguous ones ("ugh", "argh", "grr") match their plain form too.
|
|
449
|
+
// Picked to avoid hex / base64 contamination via the surrounding `\b` plus
|
|
450
|
+
// letter-only alternatives.
|
|
481
451
|
const ANGUISH_PATTERNS: readonly string[] = [
|
|
482
452
|
"no{3,}", // nooo, noooooo
|
|
483
453
|
"a+h{2,}", // ahh, aaaahhh
|
|
484
|
-
"u+g+h
|
|
454
|
+
"u+r?g+h+", // ugh, ughh, urgh, uuugh
|
|
485
455
|
"a+r+g+h+", // argh, aaargh, arrgghhh
|
|
456
|
+
"g+r{2,}", // grr, grrrr
|
|
486
457
|
"st+o{3,}p+", // stooop, sttooopp
|
|
487
458
|
"w+h+y{3,}", // whyyy, whyyyyy
|
|
488
459
|
"f+u{3,}c*k*", // fuuu, fuuuck
|
|
@@ -494,9 +465,13 @@ const ANGUISH_PATTERNS: readonly string[] = [
|
|
|
494
465
|
];
|
|
495
466
|
const ANGUISH_RE = new RegExp(String.raw`\b(?:${ANGUISH_PATTERNS.join("|")})\b`, "gi");
|
|
496
467
|
const DUDE_RE = /\bdude\b/gi;
|
|
497
|
-
//
|
|
498
|
-
//
|
|
499
|
-
|
|
468
|
+
// Sad emoticons (`:(` and friends). Requires a leading boundary so pasted
|
|
469
|
+
// code/regex fragments like `foo:(bar)` don't fire; `\(+` folds `:(((` into
|
|
470
|
+
// one hit. 46 corpus hits, ~90% genuinely deflated.
|
|
471
|
+
const SAD_EMOTICON_RE = /(?<=^|[\s.!?])[:;]-?\(+/g;
|
|
472
|
+
// Dot runs (`..` / `...` / `....`) are deliberately NOT counted: on a real
|
|
473
|
+
// corpus they are dominated by neutral trail-offs, template placeholders
|
|
474
|
+
// (`{{href...}}`) and range syntax, not exasperation.
|
|
500
475
|
|
|
501
476
|
// --- Frustration signals ----------------------------------------------------
|
|
502
477
|
// Each set of patterns below is tuned against ~42k real user prompts so the
|
|
@@ -506,10 +481,18 @@ const ELLIPSIS_RE = /\.{2,}/g;
|
|
|
506
481
|
// trimmed prose body (no `m` flag) - in practice mid-message lines that
|
|
507
482
|
// start with `no`/`Wrong`/`No JSDoc warning` are list items, pasted error
|
|
508
483
|
// text or descriptive statements, not actual corrections. Real frustration
|
|
509
|
-
// negation overwhelmingly opens the message.
|
|
510
|
-
|
|
484
|
+
// negation overwhelmingly opens the message. Bare `no` only counts as an
|
|
485
|
+
// interjection - followed by punctuation, end-of-text, or a discourse word -
|
|
486
|
+
// so determiner uses ("no extensions to the page", "no auto start") don't
|
|
487
|
+
// score. A hyphen only counts as a separator when it doesn't glue a compound
|
|
488
|
+
// (`no - not that` yes, `no-op change` no). Note: the bare-`no` branch
|
|
489
|
+
// deliberately has no trailing `\b`, so emphatic misspellings like `nou ...`
|
|
490
|
+
// / `nono ...` also count (via `no` + follower `u`/`no`) - on the real
|
|
491
|
+
// corpus every such hit is a genuine correction.
|
|
492
|
+
const NEGATION_LEAD_RE =
|
|
493
|
+
/^[ \t]*(?:(?:nope|nah|nvm|wrong|incorrect)\b|no(?=\s*(?:[,.!?;:\u2013\u2014]|-(?!\w)|$|(?:i|im|u|you|ur|we|it|its|that|thats|this|the|they|theyre|he|she|man|dude|bro|wait|dont|not|stop|just|again|please|plz|but|actually|literally|seriously|sorry|no|never|nothing|wtf|why|what|wrong)\b)))/gi;
|
|
511
494
|
const NEGATION_PHRASE_RE =
|
|
512
|
-
/\b(?:that['\u2019]?s\s+not\s+(?:what|right|it)|not\s+what\s+i\s+(?:meant|asked|said|wanted))\b/gi;
|
|
495
|
+
/\b(?:that['\u2019]?s\s+not\s+(?:what|right|it)|not\s+what\s+i\s+(?:meant|asked|said|wanted)|makes\s+(?:no|zero)\s+sense)\b/gi;
|
|
513
496
|
|
|
514
497
|
// User repeating themselves. The recall pattern accepts an optional
|
|
515
498
|
// `like ` / `as ` prefix so "like i said" doesn't double-count with bare
|
|
@@ -526,6 +509,10 @@ const REPETITION_STILL_RE =
|
|
|
526
509
|
// Direct second-person reproach. `you` alone is too generic (>7k hits in
|
|
527
510
|
// short prose), so we anchor it to a small set of accusatory verbs.
|
|
528
511
|
const BLAME_YOU_RE = /\byou\s+(?:didn['\u2019]?t|did\s+not|broke|missed|forgot|keep|always|never|still|ignored)\b/gi;
|
|
512
|
+
// `why would/did you ...` is reproach even when politely phrased. Bare
|
|
513
|
+
// `why you` / `why are you` are dominated by neutral how-does-this-work
|
|
514
|
+
// questions, so only the past/conditional forms count.
|
|
515
|
+
const BLAME_WHY_RE = /\bwhy\s+(?:would|did)\s+(?:you|u)\b/gi;
|
|
529
516
|
// `stop <verb>ing` is only frustration when it's an imperative - require it
|
|
530
517
|
// to start a sentence (line start or after a sentence-terminating punctuator).
|
|
531
518
|
const BLAME_STOP_RE = /(?:^|(?<=[.!?\n]))\s*stop\s+\w+ing\b/gim;
|
|
@@ -540,6 +527,11 @@ const XML_TAG_BARE_RE = /<\/?[A-Za-z][\w-]*\b[^>]*\/?>/g;
|
|
|
540
527
|
const INLINE_CODE_RE = /`[^`\n]*`/g;
|
|
541
528
|
const URL_RE = /\bhttps?:\/\/\S+/gi;
|
|
542
529
|
const FILE_MENTION_RE = /(^|\s)@[\w./-]+/g;
|
|
530
|
+
// Dotted tokens: filenames (`AGENTS.md`), dotted identifiers (`Bun.file`),
|
|
531
|
+
// versions (`1.2.3`). Stripped so SENTENCE_RE doesn't split them into
|
|
532
|
+
// all-caps fragments ("Follow AGENTS.md and ..." -> "Follow AGENTS") that
|
|
533
|
+
// register as yelling.
|
|
534
|
+
const DOTTED_TOKEN_RE = /(?<=^|[\s("'[])[\w-]+(?:\.[\w-]+)+(?=$|[\s)"'\],:;!?]|\.(?!\w))/g;
|
|
543
535
|
const QUOTE_LINE_RE = /^[ \t]*>.*$/gm;
|
|
544
536
|
// Harness placeholders the TUI substitutes for binary/non-text user input.
|
|
545
537
|
// Strip them so real frustration signals on later lines aren't masked off
|
|
@@ -561,11 +553,27 @@ function countMatches(text: string, re: RegExp): number {
|
|
|
561
553
|
return count;
|
|
562
554
|
}
|
|
563
555
|
|
|
556
|
+
// A sentence needs 2+ uppercase runs ("WHAT THE HELL"), or a single elongated
|
|
557
|
+
// run with a tripled letter ("CMOOON"), before its caps ratio can count as
|
|
558
|
+
// yelling. A lone acronym / identifier / env var ("Follow AGENTS", "use
|
|
559
|
+
// JSON", "HOME=/tmp") never does.
|
|
560
|
+
const UPPER_RUN_RE = /\p{Lu}{2,}/gu;
|
|
561
|
+
const TRIPLED_LETTER_RE = /(\p{Lu})\1\1/u;
|
|
562
|
+
|
|
563
|
+
function isShoutedSentence(sentence: string): boolean {
|
|
564
|
+
const runs = sentence.match(UPPER_RUN_RE);
|
|
565
|
+
if (!runs) return false;
|
|
566
|
+
if (runs.length >= 2) return true;
|
|
567
|
+
return runs[0].length >= YELLING_MIN_LETTERS && TRIPLED_LETTER_RE.test(runs[0]);
|
|
568
|
+
}
|
|
569
|
+
|
|
564
570
|
/**
|
|
565
571
|
* Count sentences where the share of uppercase letters exceeds
|
|
566
572
|
* {@link YELLING_THRESHOLD}. Sentences shorter than
|
|
567
573
|
* {@link YELLING_MIN_LETTERS} alphabetic characters are ignored so that
|
|
568
|
-
* short acronyms ("OK", "WIP", "TODO") don't register as yelling
|
|
574
|
+
* short acronyms ("OK", "WIP", "TODO") don't register as yelling, and the
|
|
575
|
+
* caps must span multiple words (or one elongated shout) - see
|
|
576
|
+
* {@link isShoutedSentence}.
|
|
569
577
|
*/
|
|
570
578
|
function countYellingSentences(text: string): number {
|
|
571
579
|
let count = 0;
|
|
@@ -576,7 +584,7 @@ function countYellingSentences(text: string): number {
|
|
|
576
584
|
const letters = countMatches(sentence, LETTER_RE);
|
|
577
585
|
if (letters >= YELLING_MIN_LETTERS) {
|
|
578
586
|
const upper = countMatches(sentence, UPPER_LETTER_RE);
|
|
579
|
-
if (upper / letters > YELLING_THRESHOLD) count++;
|
|
587
|
+
if (upper / letters > YELLING_THRESHOLD && isShoutedSentence(sentence)) count++;
|
|
580
588
|
}
|
|
581
589
|
match = SENTENCE_RE.exec(text);
|
|
582
590
|
}
|
|
@@ -597,6 +605,7 @@ function stripStructuredContent(text: string): string {
|
|
|
597
605
|
.replace(INLINE_CODE_RE, " ")
|
|
598
606
|
.replace(URL_RE, " ")
|
|
599
607
|
.replace(FILE_MENTION_RE, "$1 ")
|
|
608
|
+
.replace(DOTTED_TOKEN_RE, " ")
|
|
600
609
|
.replace(QUOTE_LINE_RE, "")
|
|
601
610
|
.replace(IMAGE_MARKER_RE, " ")
|
|
602
611
|
.replace(ANSI_ESCAPE_RE, "");
|
|
@@ -654,11 +663,12 @@ export function computeUserMessageMetrics(text: string): UserMessageMetrics {
|
|
|
654
663
|
countMatches(prose, DRAMA_RE) +
|
|
655
664
|
countMatches(prose, ANGUISH_RE) +
|
|
656
665
|
countMatches(prose, DUDE_RE) +
|
|
657
|
-
countMatches(prose,
|
|
666
|
+
countMatches(prose, SAD_EMOTICON_RE);
|
|
658
667
|
|
|
659
668
|
const negation = countMatches(prose, NEGATION_LEAD_RE) + countMatches(prose, NEGATION_PHRASE_RE);
|
|
660
669
|
const repetition = countMatches(prose, REPETITION_RECALL_RE) + countMatches(prose, REPETITION_STILL_RE);
|
|
661
|
-
const blame =
|
|
670
|
+
const blame =
|
|
671
|
+
countMatches(prose, BLAME_YOU_RE) + countMatches(prose, BLAME_WHY_RE) + countMatches(prose, BLAME_STOP_RE);
|
|
662
672
|
|
|
663
673
|
return {
|
|
664
674
|
chars,
|