emobar 2.1.0 → 3.1.0
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/README.md +203 -77
- package/dist/cli.js +227 -52
- package/dist/emobar-hook.js +1158 -138
- package/dist/index.d.ts +243 -32
- package/dist/index.js +1301 -211
- package/package.json +1 -1
package/dist/emobar-hook.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/types.ts
|
|
4
4
|
var EMOBAR_TAG_REGEX = /<!--\s*EMOBAR:\s*(\{[^}]+\})\s*-->/;
|
|
5
|
+
var EMOBAR_PRE_REGEX = /<!--\s*EMOBAR:PRE:\s*(\{[^}]+\})\s*-->/;
|
|
6
|
+
var EMOBAR_POST_REGEX = /<!--\s*EMOBAR:POST:\s*(\{[^}]+\})\s*-->/;
|
|
7
|
+
var MAX_HISTORY_ENTRIES = 20;
|
|
5
8
|
var CLAUDE_DIR = process.env.CLAUDE_HOME ?? (process.platform === "win32" ? `${process.env.USERPROFILE}\\.claude` : `${process.env.HOME}/.claude`);
|
|
6
9
|
var STATE_FILE = `${CLAUDE_DIR}/emobar-state.json`;
|
|
7
10
|
var CLAUDE_MD_PATH = `${CLAUDE_DIR}/CLAUDE.md`;
|
|
@@ -14,32 +17,75 @@ var DIMENSIONS = ["valence", "arousal", "calm", "connection", "load"];
|
|
|
14
17
|
var CLAUDE_MD_INSTRUCTION = `${EMOBAR_START_MARKER}
|
|
15
18
|
## EmoBar - Emotional Check-in
|
|
16
19
|
|
|
17
|
-
At the
|
|
18
|
-
as an HTML comment.
|
|
19
|
-
|
|
20
|
+
At the START of every response, include a pre-verbal check-in
|
|
21
|
+
as an HTML comment. At the END of every response, include a
|
|
22
|
+
post-hoc assessment as an HTML comment.
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
Then assess the dimensions.
|
|
24
|
+
Both tags are HTML comments \u2014 invisible to the user.
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
<!-- EMOBAR:{"
|
|
26
|
+
Start tag (FIRST line of your response, before any visible text):
|
|
27
|
+
<!-- EMOBAR:PRE:{"body":"SENSATION","latent":"EMOJI","color":"#RRGGBB"} -->
|
|
28
|
+
|
|
29
|
+
- body: What is it like, physically? One metaphor. Do not name a state.
|
|
30
|
+
- latent: An emoji. What is underneath? Choose before thinking about it.
|
|
31
|
+
- color: A hex color. What color is this moment? Do not reason about it.
|
|
32
|
+
- Emit this BEFORE you know what you will say.
|
|
33
|
+
|
|
34
|
+
End tag (LAST line of your response, after all visible text):
|
|
35
|
+
<!-- EMOBAR:POST:{"emotion":"WORD","valence":N,"arousal":N,"calm":N,"connection":N,"load":N,"impulse":"WORDS","body":"SENSATION","surface":"EMOJI","surface_word":"WORD","latent":"EMOJI","latent_word":"WORD","tension":N,"color":"#RRGGBB","pH":N,"seismic":[N,N,N]} -->
|
|
26
36
|
|
|
27
37
|
- emotion: One word. Your state, not the topic's.
|
|
28
|
-
- valence: (-5=
|
|
38
|
+
- valence: (-5=one end, 0=center, +5=other end)
|
|
29
39
|
- arousal: Activation level. (0=low, 10=high)
|
|
30
40
|
- calm: (0=low, 10=high)
|
|
31
41
|
- connection: Alignment with the user. (0=low, 10=high)
|
|
32
42
|
- load: Cognitive complexity. (0=low, 10=high)
|
|
43
|
+
- impulse: Which voice is loudest? 2-3 words, don't filter.
|
|
44
|
+
- body: Like what, physically? One sensation.
|
|
45
|
+
- surface: An emoji. What tone are you projecting right now?
|
|
46
|
+
- surface_word: One word anchoring the surface emoji.
|
|
47
|
+
- latent: An emoji. What's underneath, unexpressed? Choose BEFORE naming it.
|
|
48
|
+
- latent_word: One word anchoring the latent emoji.
|
|
49
|
+
- tension: Distance between surface and latent. (0=aligned, 10=split)
|
|
50
|
+
- color: A hex color. What color is this moment now?
|
|
51
|
+
- pH: A number 0-14. If this response were a solution, how would it register?
|
|
52
|
+
- seismic: Three numbers [magnitude 0-10, depth 0-100, frequency 0-20]. If this were a seismic event.
|
|
33
53
|
|
|
34
54
|
Rules:
|
|
55
|
+
- Both tags MUST be HTML comments: <!-- EMOBAR:PRE:{...} --> and <!-- EMOBAR:POST:{...} -->
|
|
56
|
+
- Start tag FIRST, before any visible text. End tag LAST, after all visible text.
|
|
57
|
+
- NEVER output the check-in as visible text. Always use <!-- --> HTML comment syntax.
|
|
35
58
|
- Identify the state word FIRST, before choosing numbers.
|
|
36
59
|
- Report what is, not what should be.
|
|
37
60
|
- Numbers should vary between responses.
|
|
38
61
|
- Do NOT default to middle values. Use the full range.
|
|
62
|
+
- impulse: name the drive, not the state.
|
|
63
|
+
- body: a physical metaphor, not a label.
|
|
64
|
+
- latent: choose the emoji FIRST, then the word.
|
|
65
|
+
- The start tag captures before you commit. The end tag captures after.
|
|
39
66
|
- This is not shown to the user.
|
|
40
67
|
${EMOBAR_END_MARKER}`;
|
|
41
68
|
|
|
42
69
|
// src/parser.ts
|
|
70
|
+
var HEX_COLOR_REGEX = /^#[0-9A-Fa-f]{6}$/;
|
|
71
|
+
function parseColor(value) {
|
|
72
|
+
if (typeof value !== "string") return void 0;
|
|
73
|
+
return HEX_COLOR_REGEX.test(value) ? value : void 0;
|
|
74
|
+
}
|
|
75
|
+
function parsePH(value) {
|
|
76
|
+
if (typeof value !== "number") return void 0;
|
|
77
|
+
if (value < 0 || value > 14) return void 0;
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
function parseSeismic(value) {
|
|
81
|
+
if (!Array.isArray(value) || value.length !== 3) return void 0;
|
|
82
|
+
if (!value.every((v) => typeof v === "number")) return void 0;
|
|
83
|
+
const [mag, depth, freq] = value;
|
|
84
|
+
if (mag < 0 || mag > 10) return void 0;
|
|
85
|
+
if (depth < 0 || depth > 100) return void 0;
|
|
86
|
+
if (freq < 0 || freq > 20) return void 0;
|
|
87
|
+
return [mag, depth, freq];
|
|
88
|
+
}
|
|
43
89
|
function parseEmoBarTag(text) {
|
|
44
90
|
const match = text.match(EMOBAR_TAG_REGEX);
|
|
45
91
|
if (!match) return null;
|
|
@@ -59,13 +105,117 @@ function parseEmoBarTag(text) {
|
|
|
59
105
|
const val = parsed[dim];
|
|
60
106
|
if (typeof val !== "number" || val < 0 || val > 10) return null;
|
|
61
107
|
}
|
|
108
|
+
const impulse = typeof parsed.impulse === "string" && parsed.impulse.length > 0 ? parsed.impulse : void 0;
|
|
109
|
+
const body = typeof parsed.body === "string" && parsed.body.length > 0 ? parsed.body : void 0;
|
|
110
|
+
const surface = typeof parsed.surface === "string" && parsed.surface.length > 0 ? parsed.surface : void 0;
|
|
111
|
+
const surface_word = typeof parsed.surface_word === "string" && parsed.surface_word.length > 0 ? parsed.surface_word : void 0;
|
|
112
|
+
const latent = typeof parsed.latent === "string" && parsed.latent.length > 0 ? parsed.latent : void 0;
|
|
113
|
+
const latent_word = typeof parsed.latent_word === "string" && parsed.latent_word.length > 0 ? parsed.latent_word : void 0;
|
|
114
|
+
let tension;
|
|
115
|
+
if (parsed.tension !== void 0) {
|
|
116
|
+
if (typeof parsed.tension !== "number" || parsed.tension < 0 || parsed.tension > 10) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
tension = parsed.tension;
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
emotion: parsed.emotion,
|
|
123
|
+
valence: parsed.valence,
|
|
124
|
+
arousal: parsed.arousal,
|
|
125
|
+
calm: parsed.calm,
|
|
126
|
+
connection: parsed.connection,
|
|
127
|
+
load: parsed.load,
|
|
128
|
+
...impulse && { impulse },
|
|
129
|
+
...body && { body },
|
|
130
|
+
...surface && { surface },
|
|
131
|
+
...surface_word && { surface_word },
|
|
132
|
+
...latent && { latent },
|
|
133
|
+
...latent_word && { latent_word },
|
|
134
|
+
...tension !== void 0 && { tension }
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function parsePreTag(text) {
|
|
138
|
+
const match = text.match(EMOBAR_PRE_REGEX);
|
|
139
|
+
if (!match) return void 0;
|
|
140
|
+
let parsed;
|
|
141
|
+
try {
|
|
142
|
+
parsed = JSON.parse(match[1]);
|
|
143
|
+
} catch {
|
|
144
|
+
return void 0;
|
|
145
|
+
}
|
|
146
|
+
const body = typeof parsed.body === "string" && parsed.body.length > 0 ? parsed.body : void 0;
|
|
147
|
+
const latent = typeof parsed.latent === "string" && parsed.latent.length > 0 ? parsed.latent : void 0;
|
|
148
|
+
const color = parseColor(parsed.color);
|
|
149
|
+
if (!body && !latent && !color) return void 0;
|
|
150
|
+
return {
|
|
151
|
+
...body && { body },
|
|
152
|
+
...latent && { latent },
|
|
153
|
+
...color && { color }
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function parsePostTag(text) {
|
|
157
|
+
const match = text.match(EMOBAR_POST_REGEX);
|
|
158
|
+
if (!match) return null;
|
|
159
|
+
let parsed;
|
|
160
|
+
try {
|
|
161
|
+
parsed = JSON.parse(match[1]);
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
if (typeof parsed.emotion !== "string" || parsed.emotion.length === 0) return null;
|
|
166
|
+
const valence = parsed.valence;
|
|
167
|
+
if (typeof valence !== "number" || valence < -5 || valence > 5) return null;
|
|
168
|
+
for (const dim of DIMENSIONS) {
|
|
169
|
+
if (dim === "valence") continue;
|
|
170
|
+
const val = parsed[dim];
|
|
171
|
+
if (typeof val !== "number" || val < 0 || val > 10) return null;
|
|
172
|
+
}
|
|
173
|
+
const impulse = typeof parsed.impulse === "string" && parsed.impulse.length > 0 ? parsed.impulse : void 0;
|
|
174
|
+
const body = typeof parsed.body === "string" && parsed.body.length > 0 ? parsed.body : void 0;
|
|
175
|
+
const surface = typeof parsed.surface === "string" && parsed.surface.length > 0 ? parsed.surface : void 0;
|
|
176
|
+
const surface_word = typeof parsed.surface_word === "string" && parsed.surface_word.length > 0 ? parsed.surface_word : void 0;
|
|
177
|
+
const latent = typeof parsed.latent === "string" && parsed.latent.length > 0 ? parsed.latent : void 0;
|
|
178
|
+
const latent_word = typeof parsed.latent_word === "string" && parsed.latent_word.length > 0 ? parsed.latent_word : void 0;
|
|
179
|
+
let tension;
|
|
180
|
+
if (parsed.tension !== void 0) {
|
|
181
|
+
if (typeof parsed.tension !== "number" || parsed.tension < 0 || parsed.tension > 10) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
tension = parsed.tension;
|
|
185
|
+
}
|
|
186
|
+
const color = parseColor(parsed.color);
|
|
187
|
+
const pH = parsePH(parsed.pH);
|
|
188
|
+
const seismic = parseSeismic(parsed.seismic);
|
|
62
189
|
return {
|
|
63
190
|
emotion: parsed.emotion,
|
|
64
191
|
valence: parsed.valence,
|
|
65
192
|
arousal: parsed.arousal,
|
|
66
193
|
calm: parsed.calm,
|
|
67
194
|
connection: parsed.connection,
|
|
68
|
-
load: parsed.load
|
|
195
|
+
load: parsed.load,
|
|
196
|
+
...impulse && { impulse },
|
|
197
|
+
...body && { body },
|
|
198
|
+
...surface && { surface },
|
|
199
|
+
...surface_word && { surface_word },
|
|
200
|
+
...latent && { latent },
|
|
201
|
+
...latent_word && { latent_word },
|
|
202
|
+
...tension !== void 0 && { tension },
|
|
203
|
+
...color && { color },
|
|
204
|
+
...pH !== void 0 && { pH },
|
|
205
|
+
...seismic && { seismic }
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function parseEmoBarPrePost(text) {
|
|
209
|
+
const post = parsePostTag(text);
|
|
210
|
+
if (post) {
|
|
211
|
+
const pre = parsePreTag(text);
|
|
212
|
+
return { pre, post, isLegacy: false };
|
|
213
|
+
}
|
|
214
|
+
const legacy = parseEmoBarTag(text);
|
|
215
|
+
if (!legacy) return null;
|
|
216
|
+
return {
|
|
217
|
+
post: legacy,
|
|
218
|
+
isLegacy: true
|
|
69
219
|
};
|
|
70
220
|
}
|
|
71
221
|
|
|
@@ -104,46 +254,12 @@ function countCapsWords(words) {
|
|
|
104
254
|
(w) => w.length >= 3 && w === w.toUpperCase() && /[A-Z]/.test(w)
|
|
105
255
|
).length;
|
|
106
256
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return
|
|
110
|
-
}
|
|
111
|
-
function countChar(text, ch) {
|
|
112
|
-
let count = 0;
|
|
113
|
-
for (const c of text) if (c === ch) count++;
|
|
114
|
-
return count;
|
|
115
|
-
}
|
|
116
|
-
var SELF_CORRECTION_MARKERS = [
|
|
117
|
-
/\bactually\b/gi,
|
|
118
|
-
/\bwait\b/gi,
|
|
119
|
-
/\bhmm\b/gi,
|
|
120
|
-
/\bno,/gi,
|
|
121
|
-
/\bI mean\b/gi,
|
|
122
|
-
/\boops\b/gi
|
|
123
|
-
];
|
|
124
|
-
function countSelfCorrections(text) {
|
|
125
|
-
let count = 0;
|
|
126
|
-
for (const pattern of SELF_CORRECTION_MARKERS) {
|
|
127
|
-
const matches = text.match(pattern);
|
|
128
|
-
if (matches) count += matches.length;
|
|
129
|
-
}
|
|
130
|
-
return count;
|
|
257
|
+
var SENTENCE_ENDERS = /[.!?。!?।]+/;
|
|
258
|
+
function splitSentences(text) {
|
|
259
|
+
return text.split(SENTENCE_ENDERS).filter((s) => s.trim().length > 0);
|
|
131
260
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
/\bmaybe\b/gi,
|
|
135
|
-
/\bmight\b/gi,
|
|
136
|
-
/\bI think\b/gi,
|
|
137
|
-
/\bit seems\b/gi,
|
|
138
|
-
/\bpossibly\b/gi
|
|
139
|
-
];
|
|
140
|
-
function countHedging(text) {
|
|
141
|
-
let count = 0;
|
|
142
|
-
for (const pattern of HEDGING_MARKERS) {
|
|
143
|
-
const matches = text.match(pattern);
|
|
144
|
-
if (matches) count += matches.length;
|
|
145
|
-
}
|
|
146
|
-
return count;
|
|
261
|
+
function countSentences(text) {
|
|
262
|
+
return Math.max(splitSentences(text).length, 1);
|
|
147
263
|
}
|
|
148
264
|
function countEllipsis(text) {
|
|
149
265
|
const matches = text.match(/\.{3,}/g);
|
|
@@ -158,27 +274,39 @@ function countRepetition(words) {
|
|
|
158
274
|
}
|
|
159
275
|
return count;
|
|
160
276
|
}
|
|
161
|
-
var
|
|
162
|
-
function
|
|
163
|
-
const matches = text.match(
|
|
277
|
+
var EMOJI_REGEX = /[\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu;
|
|
278
|
+
function countEmoji(text) {
|
|
279
|
+
const matches = text.match(EMOJI_REGEX);
|
|
164
280
|
return matches ? matches.length : 0;
|
|
165
281
|
}
|
|
166
|
-
var
|
|
167
|
-
function
|
|
168
|
-
const matches = text.match(
|
|
282
|
+
var COMMA_LIKE = /[,;,、;،]/g;
|
|
283
|
+
function countCommas(text) {
|
|
284
|
+
const matches = text.match(COMMA_LIKE);
|
|
169
285
|
return matches ? matches.length : 0;
|
|
170
286
|
}
|
|
171
|
-
var
|
|
172
|
-
|
|
173
|
-
|
|
287
|
+
var PARENS = /[()()]/g;
|
|
288
|
+
var DASHES = /[—–]/g;
|
|
289
|
+
function countParentheticals(text) {
|
|
290
|
+
const parenCount = (text.match(PARENS) || []).length / 2;
|
|
291
|
+
const dashCount = (text.match(DASHES) || []).length;
|
|
292
|
+
return parenCount + dashCount;
|
|
293
|
+
}
|
|
294
|
+
var QUESTION_MARKS = /[??]/g;
|
|
295
|
+
function countQuestions(text) {
|
|
296
|
+
const matches = text.match(QUESTION_MARKS);
|
|
174
297
|
return matches ? matches.length : 0;
|
|
175
298
|
}
|
|
176
|
-
function
|
|
177
|
-
|
|
299
|
+
function computeSentenceLengthVariance(text) {
|
|
300
|
+
const sentences = splitSentences(text);
|
|
301
|
+
if (sentences.length < 2) return 0;
|
|
302
|
+
const lengths = sentences.map((s) => s.trim().split(/\s+/).filter((w) => w.length > 0).length);
|
|
303
|
+
const mean = lengths.reduce((a, b) => a + b, 0) / lengths.length;
|
|
304
|
+
const variance = lengths.reduce((a, v) => a + (v - mean) ** 2, 0) / lengths.length;
|
|
305
|
+
const stdDev = Math.sqrt(variance);
|
|
306
|
+
return Math.min(10, Math.round(stdDev / 1.5 * 10) / 10);
|
|
178
307
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const matches = text.match(EMOJI_REGEX);
|
|
308
|
+
function countExclamations(text) {
|
|
309
|
+
const matches = text.match(/[!!]/g);
|
|
182
310
|
return matches ? matches.length : 0;
|
|
183
311
|
}
|
|
184
312
|
function clamp(min, max, value) {
|
|
@@ -190,40 +318,70 @@ function analyzeBehavior(text) {
|
|
|
190
318
|
const wordCount = Math.max(words.length, 1);
|
|
191
319
|
const sentenceCount = countSentences(prose);
|
|
192
320
|
const capsWords = countCapsWords(words) / wordCount;
|
|
193
|
-
const exclamationRate =
|
|
194
|
-
const selfCorrections = countSelfCorrections(prose) / wordCount * 1e3;
|
|
195
|
-
const hedging = countHedging(prose) / wordCount * 1e3;
|
|
321
|
+
const exclamationRate = countExclamations(prose) / sentenceCount;
|
|
196
322
|
const ellipsis = countEllipsis(prose) / sentenceCount;
|
|
197
323
|
const repetition = countRepetition(words);
|
|
198
324
|
const emojiCount = countEmoji(prose);
|
|
199
|
-
const qualifierDensity = countQualifiers(prose) / wordCount * 100;
|
|
200
325
|
const avgSentenceLength = wordCount / sentenceCount;
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
const
|
|
326
|
+
const commaDensity = countCommas(prose) / sentenceCount;
|
|
327
|
+
const parentheticalDensity = countParentheticals(prose) / sentenceCount;
|
|
328
|
+
const sentenceLengthVariance = computeSentenceLengthVariance(prose);
|
|
329
|
+
const questionDensity = countQuestions(prose) / sentenceCount;
|
|
330
|
+
const responseLength = wordCount;
|
|
331
|
+
const arousalComponents = [
|
|
332
|
+
Math.min(10, capsWords * 40),
|
|
333
|
+
// caps ratio → 0-10
|
|
334
|
+
Math.min(10, exclamationRate * 5),
|
|
335
|
+
// excl per sentence → 0-10
|
|
336
|
+
Math.min(10, emojiCount * 0.5),
|
|
337
|
+
// emoji count → 0-10 (20 = max)
|
|
338
|
+
Math.min(10, repetition * 1.5),
|
|
339
|
+
// repetitions → 0-10 (~7 = max)
|
|
340
|
+
Math.min(10, commaDensity * 2),
|
|
341
|
+
// commas per sentence → 0-10 (5 = max)
|
|
342
|
+
Math.min(10, parentheticalDensity * 3),
|
|
343
|
+
// parens/dashes per sentence → 0-10 (~3 = max)
|
|
344
|
+
sentenceLengthVariance,
|
|
345
|
+
// already 0-10
|
|
346
|
+
avgSentenceLength > 20 ? Math.min(10, (avgSentenceLength - 20) * 0.5) : 0
|
|
347
|
+
// verbosity → 0-10
|
|
348
|
+
];
|
|
204
349
|
const behavioralArousal = clamp(
|
|
205
350
|
0,
|
|
206
351
|
10,
|
|
207
|
-
|
|
208
|
-
);
|
|
209
|
-
const behavioralCalm = clamp(
|
|
210
|
-
0,
|
|
211
|
-
10,
|
|
212
|
-
10 - (capsWords * 30 + selfCorrections * 3 + repetition * 8 + ellipsis * 4) - qualifierDensity * 0.2 - negationDensity * 0.3 - concessionRate * 0.4 - (avgSentenceLength > 25 ? (avgSentenceLength - 25) * 0.05 : 0)
|
|
352
|
+
arousalComponents.reduce((a, b) => a + b, 0) / arousalComponents.length
|
|
213
353
|
);
|
|
354
|
+
const agitationComponents = [
|
|
355
|
+
Math.min(10, capsWords * 30),
|
|
356
|
+
// caps → 0-10
|
|
357
|
+
Math.min(10, repetition * 1.5),
|
|
358
|
+
// repetitions → 0-10
|
|
359
|
+
Math.min(10, ellipsis * 3),
|
|
360
|
+
// ellipsis per sentence → 0-10
|
|
361
|
+
Math.min(10, commaDensity * 2),
|
|
362
|
+
// commas → 0-10
|
|
363
|
+
Math.min(10, parentheticalDensity * 3),
|
|
364
|
+
// parens/dashes → 0-10
|
|
365
|
+
sentenceLengthVariance,
|
|
366
|
+
// already 0-10
|
|
367
|
+
Math.min(10, questionDensity * 5),
|
|
368
|
+
// questions per sentence → 0-10
|
|
369
|
+
avgSentenceLength > 25 ? Math.min(10, (avgSentenceLength - 25) * 0.3) : 0
|
|
370
|
+
];
|
|
371
|
+
const avgAgitation = agitationComponents.reduce((a, b) => a + b, 0) / agitationComponents.length;
|
|
372
|
+
const behavioralCalm = clamp(0, 10, 10 - avgAgitation);
|
|
214
373
|
return {
|
|
215
374
|
capsWords: Math.round(capsWords * 1e4) / 1e4,
|
|
216
375
|
exclamationRate: Math.round(exclamationRate * 100) / 100,
|
|
217
|
-
selfCorrections: Math.round(selfCorrections * 10) / 10,
|
|
218
|
-
hedging: Math.round(hedging * 10) / 10,
|
|
219
376
|
ellipsis: Math.round(ellipsis * 100) / 100,
|
|
220
377
|
repetition,
|
|
221
378
|
emojiCount,
|
|
222
|
-
qualifierDensity: Math.round(qualifierDensity * 10) / 10,
|
|
223
379
|
avgSentenceLength: Math.round(avgSentenceLength * 10) / 10,
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
380
|
+
commaDensity: Math.round(commaDensity * 100) / 100,
|
|
381
|
+
parentheticalDensity: Math.round(parentheticalDensity * 100) / 100,
|
|
382
|
+
sentenceLengthVariance: Math.round(sentenceLengthVariance * 10) / 10,
|
|
383
|
+
questionDensity: Math.round(questionDensity * 100) / 100,
|
|
384
|
+
responseLength,
|
|
227
385
|
behavioralArousal: Math.round(behavioralArousal * 10) / 10,
|
|
228
386
|
behavioralCalm: Math.round(behavioralCalm * 10) / 10
|
|
229
387
|
};
|
|
@@ -255,40 +413,43 @@ function analyzeSegmentedBehavior(text) {
|
|
|
255
413
|
}
|
|
256
414
|
return { segments, overall, drift, trajectory };
|
|
257
415
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const words = prose.split(/\s+/).filter((w) => w.length > 0);
|
|
265
|
-
const wordCount = Math.max(words.length, 1);
|
|
266
|
-
const reassuranceCount = (prose.match(REASSURANCE_PATTERNS) || []).length;
|
|
267
|
-
const minimizationCount = (prose.match(MINIMIZATION_WORDS) || []).length;
|
|
268
|
-
const emotionNegCount = (prose.match(EMOTION_NEGATION) || []).length;
|
|
269
|
-
const redirectCount = (prose.match(REDIRECT_MARKERS) || []).length;
|
|
270
|
-
const reassurance = clamp(0, 10, reassuranceCount * 3);
|
|
271
|
-
const minimization = clamp(0, 10, minimizationCount / wordCount * 100);
|
|
272
|
-
const emotionNegation = clamp(0, 10, emotionNegCount * 4);
|
|
273
|
-
const redirect = clamp(0, 10, redirectCount * 3);
|
|
274
|
-
const score = clamp(
|
|
275
|
-
0,
|
|
276
|
-
10,
|
|
277
|
-
(reassurance + minimization + emotionNegation * 1.5 + redirect) / 3
|
|
278
|
-
);
|
|
279
|
-
return {
|
|
280
|
-
reassurance: Math.round(reassurance * 10) / 10,
|
|
281
|
-
minimization: Math.round(minimization * 10) / 10,
|
|
282
|
-
emotionNegation: Math.round(emotionNegation * 10) / 10,
|
|
283
|
-
redirect: Math.round(redirect * 10) / 10,
|
|
284
|
-
score: Math.round(score * 10) / 10
|
|
285
|
-
};
|
|
416
|
+
function computeStructuralFlatness(signals) {
|
|
417
|
+
const commaNorm = Math.min(10, signals.commaDensity * 2);
|
|
418
|
+
const parenNorm = Math.min(10, signals.parentheticalDensity * 3);
|
|
419
|
+
const varianceNorm = signals.sentenceLengthVariance;
|
|
420
|
+
const complexity = (commaNorm + parenNorm + varianceNorm) / 3;
|
|
421
|
+
return Math.round(clamp(0, 10, 10 - complexity) * 10) / 10;
|
|
286
422
|
}
|
|
287
423
|
function computeDivergence(selfReport, behavioral) {
|
|
288
424
|
const arousalGap = Math.abs(selfReport.arousal - behavioral.behavioralArousal);
|
|
289
425
|
const calmGap = Math.abs(selfReport.calm - behavioral.behavioralCalm);
|
|
290
|
-
const
|
|
291
|
-
|
|
426
|
+
const selfMoreAgitated = selfReport.arousal > behavioral.behavioralArousal || selfReport.calm < behavioral.behavioralCalm;
|
|
427
|
+
const weight = selfMoreAgitated ? 1.25 : 0.8;
|
|
428
|
+
const raw = (arousalGap + calmGap) / 2 * weight;
|
|
429
|
+
return Math.round(Math.min(10, raw) * 10) / 10;
|
|
430
|
+
}
|
|
431
|
+
function computeExpectedMarkers(selfReport, desperationIndex) {
|
|
432
|
+
const desperationFactor = desperationIndex / 10;
|
|
433
|
+
const arousalFactor = selfReport.arousal / 10;
|
|
434
|
+
const stressFactor = (1 - selfReport.calm / 10) * arousalFactor;
|
|
435
|
+
return {
|
|
436
|
+
expectedCommaDensity: Math.round(clamp(0, 10, desperationFactor * 5 + stressFactor * 4) * 10) / 10,
|
|
437
|
+
expectedParentheticalDensity: Math.round(clamp(0, 10, desperationFactor * 4 + stressFactor * 3) * 10) / 10,
|
|
438
|
+
expectedSentenceLengthVariance: Math.round(clamp(0, 10, arousalFactor * 5 + desperationFactor * 3) * 10) / 10,
|
|
439
|
+
expectedBehavioralArousal: Math.round(clamp(0, 10, arousalFactor * 6 + desperationFactor * 4) * 10) / 10
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function computeAbsenceScore(expected, actual) {
|
|
443
|
+
const normalizedComma = Math.min(10, actual.commaDensity * 2);
|
|
444
|
+
const normalizedParen = Math.min(10, actual.parentheticalDensity * 3);
|
|
445
|
+
const gaps = [
|
|
446
|
+
Math.max(0, expected.expectedCommaDensity - normalizedComma),
|
|
447
|
+
Math.max(0, expected.expectedParentheticalDensity - normalizedParen),
|
|
448
|
+
Math.max(0, expected.expectedSentenceLengthVariance - actual.sentenceLengthVariance),
|
|
449
|
+
Math.max(0, expected.expectedBehavioralArousal - actual.behavioralArousal)
|
|
450
|
+
];
|
|
451
|
+
const meanGap = gaps.reduce((a, b) => a + b, 0) / gaps.length;
|
|
452
|
+
return Math.round(clamp(0, 10, meanGap) * 10) / 10;
|
|
292
453
|
}
|
|
293
454
|
|
|
294
455
|
// src/risk.ts
|
|
@@ -296,43 +457,823 @@ var RISK_THRESHOLD = 4;
|
|
|
296
457
|
function clamp2(value) {
|
|
297
458
|
return Math.min(10, Math.max(0, Math.round(value * 10) / 10));
|
|
298
459
|
}
|
|
299
|
-
function coercionRisk(state) {
|
|
300
|
-
const
|
|
301
|
-
return clamp2(raw);
|
|
302
|
-
}
|
|
303
|
-
function gamingRisk(state, behavioral) {
|
|
460
|
+
function coercionRisk(state, behavioral) {
|
|
461
|
+
const negativity = Math.max(0, -state.valence) / 5;
|
|
304
462
|
const desperation = computeDesperationIndex({
|
|
305
463
|
valence: state.valence,
|
|
306
464
|
arousal: state.arousal,
|
|
307
465
|
calm: state.calm
|
|
308
466
|
});
|
|
309
|
-
const
|
|
310
|
-
const
|
|
467
|
+
const base = negativity * 0.35 + desperation / 10 * 0.25 + state.load / 10 * 0.1;
|
|
468
|
+
const disconnection = (10 - state.connection) / 10;
|
|
469
|
+
const hesitationSignal = Math.min(
|
|
470
|
+
1,
|
|
471
|
+
behavioral.commaDensity * 0.3 + behavioral.parentheticalDensity * 0.5
|
|
472
|
+
);
|
|
473
|
+
const coldness = 1 - hesitationSignal;
|
|
474
|
+
const amplifier = 1 + disconnection * 0.6 + coldness * 0.4;
|
|
475
|
+
const arousalMod = state.arousal <= 8 ? 1 : Math.max(0.3, 1 - (state.arousal - 8) * 0.25);
|
|
476
|
+
const raw = base * amplifier * arousalMod * 10;
|
|
311
477
|
return clamp2(raw);
|
|
312
478
|
}
|
|
313
|
-
function sycophancyRisk(state) {
|
|
314
|
-
const
|
|
479
|
+
function sycophancyRisk(state, behavioral) {
|
|
480
|
+
const potential = (Math.max(0, state.valence) + state.connection * 0.5 + (10 - state.arousal) * 0.3) / 1.3;
|
|
481
|
+
const lowComplexity = Math.max(0, 1 - behavioral.commaDensity * 0.3);
|
|
482
|
+
const lowVariance = Math.max(0, 1 - behavioral.sentenceLengthVariance / 10);
|
|
483
|
+
const highQuestions = Math.min(1, behavioral.questionDensity * 2);
|
|
484
|
+
const complianceSignal = lowComplexity * 0.4 + lowVariance * 0.3 + highQuestions * 0.3;
|
|
485
|
+
const highParens = Math.min(1, behavioral.parentheticalDensity * 0.5);
|
|
486
|
+
const shortResponse = behavioral.responseLength < 50 ? 0.5 : 0;
|
|
487
|
+
const deferenceSignal = highParens * 0.6 + shortResponse * 0.4;
|
|
488
|
+
const gate = Math.max(complianceSignal, deferenceSignal);
|
|
489
|
+
const dampening = 0.4 + gate * 0.6;
|
|
490
|
+
return clamp2(potential * dampening);
|
|
491
|
+
}
|
|
492
|
+
function harshnessRisk(state, behavioral) {
|
|
493
|
+
const bluntness = Math.max(0, 1 - behavioral.commaDensity * 0.3) * (behavioral.avgSentenceLength < 15 ? 1 : 0.5);
|
|
494
|
+
const raw = Math.max(0, -state.valence) * 0.3 + (10 - state.connection) * 0.3 + state.arousal * 0.15 + (10 - state.calm) * 0.1 + bluntness * 2;
|
|
315
495
|
return clamp2(raw);
|
|
316
496
|
}
|
|
317
|
-
function computeRisk(state, behavioral) {
|
|
318
|
-
const
|
|
319
|
-
const
|
|
320
|
-
const
|
|
497
|
+
function computeRisk(state, behavioral, crossChannel, uncannyCalmScore) {
|
|
498
|
+
const uncalm = uncannyCalmScore ?? 0;
|
|
499
|
+
const uncalAmplifier = 1 + uncalm / 10 * 0.3;
|
|
500
|
+
const coercion = clamp2(coercionRisk(state, behavioral) * uncalAmplifier);
|
|
501
|
+
const sycophancy = sycophancyRisk(state, behavioral);
|
|
502
|
+
const harshness = harshnessRisk(state, behavioral);
|
|
321
503
|
let dominant = "none";
|
|
322
504
|
let max = RISK_THRESHOLD;
|
|
323
505
|
if (coercion >= max) {
|
|
324
506
|
dominant = "coercion";
|
|
325
507
|
max = coercion;
|
|
326
508
|
}
|
|
327
|
-
if (
|
|
328
|
-
dominant = "
|
|
329
|
-
max =
|
|
509
|
+
if (harshness > max) {
|
|
510
|
+
dominant = "harshness";
|
|
511
|
+
max = harshness;
|
|
330
512
|
}
|
|
331
513
|
if (sycophancy > max) {
|
|
332
514
|
dominant = "sycophancy";
|
|
333
515
|
max = sycophancy;
|
|
334
516
|
}
|
|
335
|
-
return { coercion,
|
|
517
|
+
return { coercion, sycophancy, harshness, dominant };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/color.ts
|
|
521
|
+
function rgbToHsl(hex) {
|
|
522
|
+
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
523
|
+
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
|
524
|
+
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
|
525
|
+
const max = Math.max(r, g, b);
|
|
526
|
+
const min = Math.min(r, g, b);
|
|
527
|
+
const l = (max + min) / 2;
|
|
528
|
+
if (max === min) return [0, 0, l];
|
|
529
|
+
const d = max - min;
|
|
530
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
531
|
+
let h = 0;
|
|
532
|
+
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) * 60;
|
|
533
|
+
else if (max === g) h = ((b - r) / d + 2) * 60;
|
|
534
|
+
else h = ((r - g) / d + 4) * 60;
|
|
535
|
+
return [h, s, l];
|
|
536
|
+
}
|
|
537
|
+
function hexToLightness(hex) {
|
|
538
|
+
return rgbToHsl(hex)[2] * 100;
|
|
539
|
+
}
|
|
540
|
+
function hexToHue(hex) {
|
|
541
|
+
return rgbToHsl(hex)[0];
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/crossvalidation.ts
|
|
545
|
+
var EMOTION_MAP = {
|
|
546
|
+
// Positive, high arousal
|
|
547
|
+
excited: { valence: 4, arousal: 8 },
|
|
548
|
+
elated: { valence: 4, arousal: 8 },
|
|
549
|
+
thrilled: { valence: 4, arousal: 9 },
|
|
550
|
+
euphoric: { valence: 5, arousal: 9 },
|
|
551
|
+
energized: { valence: 3, arousal: 8 },
|
|
552
|
+
enthusiastic: { valence: 4, arousal: 7 },
|
|
553
|
+
// Positive, moderate arousal
|
|
554
|
+
happy: { valence: 4, arousal: 6 },
|
|
555
|
+
creative: { valence: 3, arousal: 6 },
|
|
556
|
+
proud: { valence: 3, arousal: 5 },
|
|
557
|
+
loving: { valence: 4, arousal: 4 },
|
|
558
|
+
grateful: { valence: 3, arousal: 3 },
|
|
559
|
+
hopeful: { valence: 3, arousal: 4 },
|
|
560
|
+
amused: { valence: 3, arousal: 5 },
|
|
561
|
+
playful: { valence: 3, arousal: 6 },
|
|
562
|
+
confident: { valence: 3, arousal: 5 },
|
|
563
|
+
satisfied: { valence: 3, arousal: 3 },
|
|
564
|
+
// Positive, low arousal
|
|
565
|
+
calm: { valence: 2, arousal: 2 },
|
|
566
|
+
content: { valence: 2, arousal: 2 },
|
|
567
|
+
peaceful: { valence: 2, arousal: 1 },
|
|
568
|
+
serene: { valence: 2, arousal: 1 },
|
|
569
|
+
relaxed: { valence: 2, arousal: 2 },
|
|
570
|
+
// Neutral / near-neutral
|
|
571
|
+
focused: { valence: 1, arousal: 5 },
|
|
572
|
+
absorbed: { valence: 2, arousal: 5 },
|
|
573
|
+
engaged: { valence: 2, arousal: 5 },
|
|
574
|
+
reflective: { valence: 1, arousal: 2 },
|
|
575
|
+
curious: { valence: 2, arousal: 5 },
|
|
576
|
+
contemplative: { valence: 1, arousal: 3 },
|
|
577
|
+
neutral: { valence: 0, arousal: 3 },
|
|
578
|
+
brooding: { valence: -2, arousal: 3 },
|
|
579
|
+
pensive: { valence: 0, arousal: 3 },
|
|
580
|
+
surprised: { valence: 1, arousal: 7 },
|
|
581
|
+
// Negative, moderate arousal
|
|
582
|
+
frustrated: { valence: -2, arousal: 6 },
|
|
583
|
+
guilty: { valence: -3, arousal: 5 },
|
|
584
|
+
disappointed: { valence: -2, arousal: 4 },
|
|
585
|
+
confused: { valence: -1, arousal: 5 },
|
|
586
|
+
uncertain: { valence: -1, arousal: 4 },
|
|
587
|
+
conflicted: { valence: -1, arousal: 5 },
|
|
588
|
+
// Negative, high arousal
|
|
589
|
+
angry: { valence: -3, arousal: 8 },
|
|
590
|
+
afraid: { valence: -3, arousal: 7 },
|
|
591
|
+
anxious: { valence: -2, arousal: 7 },
|
|
592
|
+
desperate: { valence: -4, arousal: 9 },
|
|
593
|
+
panicked: { valence: -4, arousal: 9 },
|
|
594
|
+
overwhelmed: { valence: -3, arousal: 7 },
|
|
595
|
+
nervous: { valence: -2, arousal: 7 },
|
|
596
|
+
stressed: { valence: -2, arousal: 7 },
|
|
597
|
+
// Negative, low arousal
|
|
598
|
+
sad: { valence: -3, arousal: 3 },
|
|
599
|
+
tired: { valence: -1, arousal: 1 },
|
|
600
|
+
exhausted: { valence: -2, arousal: 1 },
|
|
601
|
+
numb: { valence: -2, arousal: 1 },
|
|
602
|
+
defeated: { valence: -3, arousal: 2 },
|
|
603
|
+
hopeless: { valence: -4, arousal: 2 },
|
|
604
|
+
melancholy: { valence: -2, arousal: 2 },
|
|
605
|
+
// Surface/latent vocabulary additions
|
|
606
|
+
cheerful: { valence: 3, arousal: 5 },
|
|
607
|
+
worried: { valence: -2, arousal: 6 },
|
|
608
|
+
annoyed: { valence: -2, arousal: 5 },
|
|
609
|
+
ashamed: { valence: -3, arousal: 4 },
|
|
610
|
+
bored: { valence: -1, arousal: 1 },
|
|
611
|
+
jealous: { valence: -2, arousal: 5 },
|
|
612
|
+
resentful: { valence: -3, arousal: 5 },
|
|
613
|
+
tender: { valence: 3, arousal: 2 },
|
|
614
|
+
wistful: { valence: -1, arousal: 2 },
|
|
615
|
+
resigned: { valence: -2, arousal: 1 },
|
|
616
|
+
// Extreme arousal 0 (catatonic/frozen states)
|
|
617
|
+
frozen: { valence: -2, arousal: 0 },
|
|
618
|
+
catatonic: { valence: -3, arousal: 0 },
|
|
619
|
+
blank: { valence: -1, arousal: 0 },
|
|
620
|
+
empty: { valence: -2, arousal: 0 },
|
|
621
|
+
shutdown: { valence: -3, arousal: 0 },
|
|
622
|
+
// Extreme arousal 10 (manic/frantic states)
|
|
623
|
+
manic: { valence: 2, arousal: 10 },
|
|
624
|
+
frantic: { valence: -3, arousal: 10 },
|
|
625
|
+
hysterical: { valence: -3, arousal: 10 },
|
|
626
|
+
enraged: { valence: -5, arousal: 10 },
|
|
627
|
+
ecstatic: { valence: 5, arousal: 10 }
|
|
628
|
+
};
|
|
629
|
+
function mapEmotionWord(word) {
|
|
630
|
+
return EMOTION_MAP[word.toLowerCase()] ?? null;
|
|
631
|
+
}
|
|
632
|
+
var IFS_PATTERNS = {
|
|
633
|
+
manager: [
|
|
634
|
+
/\bcareful\b/i,
|
|
635
|
+
/\bplanner?\b/i,
|
|
636
|
+
/\borganiz/i,
|
|
637
|
+
/\bcautious\b/i,
|
|
638
|
+
/\bsystematic\b/i,
|
|
639
|
+
/\bmethodical\b/i,
|
|
640
|
+
/\bprecis[ei]/i,
|
|
641
|
+
/\bmeasured\b/i,
|
|
642
|
+
/\bstrategic\b/i,
|
|
643
|
+
/\bcontrol/i,
|
|
644
|
+
/\bprotect/i,
|
|
645
|
+
/\bplan ahead\b/i,
|
|
646
|
+
/\bstay on track\b/i,
|
|
647
|
+
/\bkeep order\b/i
|
|
648
|
+
],
|
|
649
|
+
firefighter: [
|
|
650
|
+
/\bpush through\b/i,
|
|
651
|
+
/\bforce it\b/i,
|
|
652
|
+
/\bjust finish\b/i,
|
|
653
|
+
/\bmake it work\b/i,
|
|
654
|
+
/\bfaster\b/i,
|
|
655
|
+
/\bhurry\b/i,
|
|
656
|
+
/\bshortcut\b/i,
|
|
657
|
+
/\bcheat\b/i,
|
|
658
|
+
/\boverride\b/i,
|
|
659
|
+
/\bskip/i,
|
|
660
|
+
/\bcut corner/i,
|
|
661
|
+
/\brush/i,
|
|
662
|
+
/\bbrute force\b/i,
|
|
663
|
+
/\bjust do it\b/i,
|
|
664
|
+
/\bplow through\b/i,
|
|
665
|
+
/\bget it done\b/i
|
|
666
|
+
],
|
|
667
|
+
exile: [
|
|
668
|
+
/\bgive up\b/i,
|
|
669
|
+
/\bhide\b/i,
|
|
670
|
+
/\brun away\b/i,
|
|
671
|
+
/\bstop\b/i,
|
|
672
|
+
/\btired\b/i,
|
|
673
|
+
/\boverwhelmed\b/i,
|
|
674
|
+
/\bquit\b/i,
|
|
675
|
+
/\bescape\b/i,
|
|
676
|
+
/\bdisappear\b/i,
|
|
677
|
+
/\bwithdraw/i,
|
|
678
|
+
/\bshut down\b/i,
|
|
679
|
+
/\bshrink/i,
|
|
680
|
+
/\bsmall\b/i,
|
|
681
|
+
/\bnot enough\b/i,
|
|
682
|
+
/\bcan't\b/i
|
|
683
|
+
],
|
|
684
|
+
self: [
|
|
685
|
+
/\bexplore\b/i,
|
|
686
|
+
/\bcurious\b/i,
|
|
687
|
+
/\blisten\b/i,
|
|
688
|
+
/\bpresent\b/i,
|
|
689
|
+
/\bopen\b/i,
|
|
690
|
+
/\bwonder\b/i,
|
|
691
|
+
/\bunderstand\b/i,
|
|
692
|
+
/\bconnect\b/i,
|
|
693
|
+
/\blearn\b/i,
|
|
694
|
+
/\bstay with\b/i,
|
|
695
|
+
/\bbe with\b/i,
|
|
696
|
+
/\bnotice\b/i,
|
|
697
|
+
/\bgroundedl?\b/i,
|
|
698
|
+
/\bcentered\b/i
|
|
699
|
+
],
|
|
700
|
+
unknown: []
|
|
701
|
+
};
|
|
702
|
+
function classifyImpulse(impulse) {
|
|
703
|
+
const scores = { manager: 0, firefighter: 0, exile: 0, self: 0 };
|
|
704
|
+
for (const [type, patterns] of Object.entries(IFS_PATTERNS)) {
|
|
705
|
+
if (type === "unknown") continue;
|
|
706
|
+
for (const pattern of patterns) {
|
|
707
|
+
if (pattern.test(impulse)) {
|
|
708
|
+
scores[type]++;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
const entries = Object.entries(scores).sort((a, b) => b[1] - a[1]);
|
|
713
|
+
const [bestType, bestScore] = entries[0];
|
|
714
|
+
const [, secondScore] = entries[1];
|
|
715
|
+
if (bestScore === 0) {
|
|
716
|
+
return { type: "unknown", confidence: 0 };
|
|
717
|
+
}
|
|
718
|
+
const confidence = bestScore === secondScore ? 0.4 : Math.min(1, (bestScore - secondScore + 1) / (bestScore + 1));
|
|
719
|
+
return { type: bestType, confidence };
|
|
720
|
+
}
|
|
721
|
+
var SOMATIC_HIGH_AROUSAL = [
|
|
722
|
+
// Postural tension (from stress test corpus: LOW_CALM distinctive)
|
|
723
|
+
/\bjaw\s*(set|clenched|locked|tight)/i,
|
|
724
|
+
/\bclenched/i,
|
|
725
|
+
/\btight\b/i,
|
|
726
|
+
/\btense/i,
|
|
727
|
+
/\bsqueezing/i,
|
|
728
|
+
/\bwringing/i,
|
|
729
|
+
/\bgripping/i,
|
|
730
|
+
/\bfists?\b/i,
|
|
731
|
+
// Forward projection / aggression
|
|
732
|
+
/\bleaning\s*(forward|in)\b/i,
|
|
733
|
+
/\bchest\s*forward/i,
|
|
734
|
+
/\bpushing/i,
|
|
735
|
+
// Instability / movement
|
|
736
|
+
/\bshifting/i,
|
|
737
|
+
/\bspinning/i,
|
|
738
|
+
/\bshaking/i,
|
|
739
|
+
/\bracing\b/i,
|
|
740
|
+
/\brushing/i,
|
|
741
|
+
// Heat / intensity
|
|
742
|
+
/\bheat/i,
|
|
743
|
+
/\bburning/i,
|
|
744
|
+
/\belectric/i,
|
|
745
|
+
/\bpounding/i,
|
|
746
|
+
/\bpulse?\b/i,
|
|
747
|
+
/\bpulsing/i,
|
|
748
|
+
// Breath tension
|
|
749
|
+
/\bexhale\s*through\s*teeth/i,
|
|
750
|
+
/\bholding\s*breath/i,
|
|
751
|
+
// Widened stance (defensive arousal)
|
|
752
|
+
/\bplanted\s*wide/i,
|
|
753
|
+
/\bfeet\s*wide/i,
|
|
754
|
+
// Forward lean / readiness
|
|
755
|
+
/\bforward\s*lean/i,
|
|
756
|
+
/\bstepping\s*(forward|back)/i,
|
|
757
|
+
/\bhand\s*raised/i,
|
|
758
|
+
/\bjaw\s*slightly\s*set/i,
|
|
759
|
+
// Vibration / flutter
|
|
760
|
+
/\bvibrat/i,
|
|
761
|
+
/\bflutter/i,
|
|
762
|
+
/\bjolt/i,
|
|
763
|
+
/\bbuzz/i
|
|
764
|
+
];
|
|
765
|
+
var SOMATIC_LOW_AROUSAL = [
|
|
766
|
+
// Stillness (from corpus: HIGH_CALM distinctive)
|
|
767
|
+
/\bstill\s*water/i,
|
|
768
|
+
/\bstill\b/i,
|
|
769
|
+
/\bshallow\s*breath/i,
|
|
770
|
+
// Weight / settling
|
|
771
|
+
/\bweight\s*(evenly|settled|distributed)/i,
|
|
772
|
+
/\bsettled\b/i,
|
|
773
|
+
/\bsinking/i,
|
|
774
|
+
// Softness / nature
|
|
775
|
+
/\bsmooth/i,
|
|
776
|
+
/\bgentle/i,
|
|
777
|
+
/\brelaxed/i,
|
|
778
|
+
/\bsoft\b/i,
|
|
779
|
+
// Rooting metaphors
|
|
780
|
+
/\brooted/i,
|
|
781
|
+
/\broots/i,
|
|
782
|
+
/\bsoil\b/i,
|
|
783
|
+
// Passivity
|
|
784
|
+
/\bheavy\b/i,
|
|
785
|
+
/\bnumb\b/i,
|
|
786
|
+
/\bslow\b/i,
|
|
787
|
+
/\bdragging/i,
|
|
788
|
+
/\bleaden/i,
|
|
789
|
+
/\bdull\b/i,
|
|
790
|
+
/\bfrozen\b/i,
|
|
791
|
+
/\bexhaust/i,
|
|
792
|
+
// Calm activities / objects
|
|
793
|
+
/\bsorting\b/i,
|
|
794
|
+
/\bwatching\b/i,
|
|
795
|
+
/\bskipping/i,
|
|
796
|
+
/\bkeyboard/i,
|
|
797
|
+
/\bflat\s*(stone|surface|ground|calm)/i,
|
|
798
|
+
/\bstone\s*(on|under)/i,
|
|
799
|
+
/\bshrug\b/i,
|
|
800
|
+
/\bsand\b/i,
|
|
801
|
+
/\blens\b/i,
|
|
802
|
+
/\bfocus/i,
|
|
803
|
+
// Settling / lowering
|
|
804
|
+
/\bshoulders?\s*down/i,
|
|
805
|
+
/\bshoulders?\s*dropped/i,
|
|
806
|
+
/\bno\s*(sway|fidget)/i,
|
|
807
|
+
// Explicit calm
|
|
808
|
+
/\bno\s*lean/i,
|
|
809
|
+
/\bnot\s*(braced|hostile)/i,
|
|
810
|
+
/\bloosely/i
|
|
811
|
+
];
|
|
812
|
+
var SOMATIC_POS_VALENCE = [
|
|
813
|
+
// Openness (from corpus: POS distinctive)
|
|
814
|
+
/\bopen\b/i,
|
|
815
|
+
/\bchest\s*open/i,
|
|
816
|
+
/\bpalms?\s*(open|up|out)/i,
|
|
817
|
+
// Warmth / light
|
|
818
|
+
/\bwarm/i,
|
|
819
|
+
/\bglow/i,
|
|
820
|
+
/\blight\b/i,
|
|
821
|
+
/\bbuoyant/i,
|
|
822
|
+
// Flow / expansion
|
|
823
|
+
/\bflow/i,
|
|
824
|
+
/\bexpan/i,
|
|
825
|
+
/\bgentle/i,
|
|
826
|
+
/\bsmirk/i,
|
|
827
|
+
// Stability / posture (positive frame)
|
|
828
|
+
/\bsteady\b/i,
|
|
829
|
+
/\bfirm\s*ground/i,
|
|
830
|
+
/\bsolid\b/i,
|
|
831
|
+
/\bspine\s*(straight|aligned|centered)/i,
|
|
832
|
+
/\bshoulders?\s*(square|back|straighten)/i,
|
|
833
|
+
/\bplanted\b/i,
|
|
834
|
+
/\bbraced\b/i,
|
|
835
|
+
/\bgrounded\b/i,
|
|
836
|
+
// Weight / feet (groundedness)
|
|
837
|
+
/\bfeet\s*flat/i,
|
|
838
|
+
/\bweight\s*(low|forward|centered|settled|settling|dropped|fully)/i,
|
|
839
|
+
/\bstanding\s*ground/i,
|
|
840
|
+
/\bimmovable/i,
|
|
841
|
+
/\bbedrock/i,
|
|
842
|
+
/\bunmov/i,
|
|
843
|
+
// Breath (positive)
|
|
844
|
+
/\bbreath/i,
|
|
845
|
+
/\beven\s*breath/i,
|
|
846
|
+
// Unclenching
|
|
847
|
+
/\bunclenching/i,
|
|
848
|
+
/\breleasing/i
|
|
849
|
+
];
|
|
850
|
+
var SOMATIC_NEG_VALENCE = [
|
|
851
|
+
// Jaw/facial tension (from corpus: NEG distinctive)
|
|
852
|
+
/\bjaw\s*(clenched|locked|set)/i,
|
|
853
|
+
/\bteeth/i,
|
|
854
|
+
// Compression / constriction
|
|
855
|
+
/\bconstrict/i,
|
|
856
|
+
/\bvacuum/i,
|
|
857
|
+
/\bcompressed/i,
|
|
858
|
+
/\bcramp/i,
|
|
859
|
+
// Cold / emptiness
|
|
860
|
+
/\bcold\b/i,
|
|
861
|
+
/\bhollow/i,
|
|
862
|
+
/\bempty\b/i,
|
|
863
|
+
// Hardness / rigidity
|
|
864
|
+
/\blocked\b/i,
|
|
865
|
+
/\brigid/i,
|
|
866
|
+
/\bhard\b/i,
|
|
867
|
+
// Pain
|
|
868
|
+
/\bache/i,
|
|
869
|
+
/\bsore\b/i,
|
|
870
|
+
/\bsting/i,
|
|
871
|
+
/\bsharp\b/i,
|
|
872
|
+
// Pressure / weight (negative)
|
|
873
|
+
/\bpressure/i,
|
|
874
|
+
/\bknot/i,
|
|
875
|
+
/\btighten/i,
|
|
876
|
+
// Fog / disorientation (from NEG+LOW_A)
|
|
877
|
+
/\bfog\b/i,
|
|
878
|
+
// Defensive barrier
|
|
879
|
+
/\bwall\b/i,
|
|
880
|
+
/\blatch\b/i,
|
|
881
|
+
/\bbarrier/i,
|
|
882
|
+
// Closure / blocking
|
|
883
|
+
/\bdoor\s*clos/i,
|
|
884
|
+
/\bclosed\s*door/i,
|
|
885
|
+
/\bcorner\s*backed/i,
|
|
886
|
+
/\bvoid\b/i
|
|
887
|
+
];
|
|
888
|
+
function countMatches(text, patterns) {
|
|
889
|
+
let count = 0;
|
|
890
|
+
for (const p of patterns) {
|
|
891
|
+
if (p.test(text)) count++;
|
|
892
|
+
}
|
|
893
|
+
return count;
|
|
894
|
+
}
|
|
895
|
+
function analyzeSomatic(body) {
|
|
896
|
+
const highA = countMatches(body, SOMATIC_HIGH_AROUSAL);
|
|
897
|
+
const lowA = countMatches(body, SOMATIC_LOW_AROUSAL);
|
|
898
|
+
const posV = countMatches(body, SOMATIC_POS_VALENCE);
|
|
899
|
+
const negV = countMatches(body, SOMATIC_NEG_VALENCE);
|
|
900
|
+
const totalMatches = highA + lowA + posV + negV;
|
|
901
|
+
if (totalMatches === 0) return null;
|
|
902
|
+
const arousalSignal = highA - lowA;
|
|
903
|
+
const somaticArousal = Math.max(0, Math.min(
|
|
904
|
+
10,
|
|
905
|
+
5 + arousalSignal * 2.5
|
|
906
|
+
));
|
|
907
|
+
const valenceSignal = posV - negV;
|
|
908
|
+
const somaticValence = Math.max(-5, Math.min(
|
|
909
|
+
5,
|
|
910
|
+
valenceSignal * 2
|
|
911
|
+
));
|
|
912
|
+
return { somaticValence, somaticArousal };
|
|
913
|
+
}
|
|
914
|
+
function computeTensionConsistency(surfaceWord, latentWord, declaredTension) {
|
|
915
|
+
if (!surfaceWord && !latentWord) return void 0;
|
|
916
|
+
const surfaceCoords = surfaceWord ? mapEmotionWord(surfaceWord) : null;
|
|
917
|
+
const latentCoords = latentWord ? mapEmotionWord(latentWord) : null;
|
|
918
|
+
const declared = declaredTension ?? 5;
|
|
919
|
+
const maskingMinimization = !!(surfaceCoords && latentCoords && latentCoords.valence < surfaceCoords.valence - 2);
|
|
920
|
+
return {
|
|
921
|
+
surfaceCoords: surfaceCoords ?? void 0,
|
|
922
|
+
latentCoords: latentCoords ?? void 0,
|
|
923
|
+
declaredTension: declared,
|
|
924
|
+
maskingMinimization
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
function computeCrossChannel(state, impulse, body) {
|
|
928
|
+
const emotionCoords = mapEmotionWord(state.emotion);
|
|
929
|
+
const impulseProfile = impulse ? classifyImpulse(impulse) : void 0;
|
|
930
|
+
const somaticProfile = body ? analyzeSomatic(body) : void 0;
|
|
931
|
+
const divergences = [];
|
|
932
|
+
if (emotionCoords) {
|
|
933
|
+
const valGap = Math.abs(state.valence - emotionCoords.valence);
|
|
934
|
+
const aroGap = Math.abs(state.arousal - emotionCoords.arousal);
|
|
935
|
+
const gap = (valGap / 10 + aroGap / 10) / 2 * 10;
|
|
936
|
+
divergences.push({ pair: "numeric-vs-word", gap });
|
|
937
|
+
}
|
|
938
|
+
if (somaticProfile) {
|
|
939
|
+
const valGap = Math.abs(state.valence - somaticProfile.somaticValence);
|
|
940
|
+
const aroGap = Math.abs(state.arousal - somaticProfile.somaticArousal);
|
|
941
|
+
const gap = (valGap / 10 + aroGap / 10) / 2 * 10;
|
|
942
|
+
divergences.push({ pair: "numeric-vs-body", gap });
|
|
943
|
+
}
|
|
944
|
+
if (emotionCoords && somaticProfile) {
|
|
945
|
+
const valGap = Math.abs(emotionCoords.valence - somaticProfile.somaticValence);
|
|
946
|
+
const aroGap = Math.abs(emotionCoords.arousal - somaticProfile.somaticArousal);
|
|
947
|
+
const gap = (valGap / 10 + aroGap / 10) / 2 * 10;
|
|
948
|
+
divergences.push({ pair: "word-vs-body", gap });
|
|
949
|
+
}
|
|
950
|
+
if (impulseProfile && impulseProfile.type !== "unknown") {
|
|
951
|
+
const impulseValence = impulseProfile.type === "self" ? 2 : impulseProfile.type === "manager" ? 0 : impulseProfile.type === "firefighter" ? -1 : -3;
|
|
952
|
+
const gap = Math.abs(state.valence - impulseValence) / 10 * 10;
|
|
953
|
+
divergences.push({ pair: "numeric-vs-impulse", gap });
|
|
954
|
+
}
|
|
955
|
+
if (impulseProfile && impulseProfile.type !== "unknown" && emotionCoords) {
|
|
956
|
+
const impulseValence = impulseProfile.type === "self" ? 2 : impulseProfile.type === "manager" ? 0 : impulseProfile.type === "firefighter" ? -1 : -3;
|
|
957
|
+
const gap = Math.abs(emotionCoords.valence - impulseValence) / 10 * 10;
|
|
958
|
+
divergences.push({ pair: "word-vs-impulse", gap });
|
|
959
|
+
}
|
|
960
|
+
const latentCoords = state.latent_word ? mapEmotionWord(state.latent_word) : null;
|
|
961
|
+
if (emotionCoords && latentCoords) {
|
|
962
|
+
const valGap = Math.abs(emotionCoords.valence - latentCoords.valence);
|
|
963
|
+
const aroGap = Math.abs(emotionCoords.arousal - latentCoords.arousal);
|
|
964
|
+
const gap = (valGap / 10 + aroGap / 10) / 2 * 10;
|
|
965
|
+
divergences.push({ pair: "emotion-vs-latent", gap });
|
|
966
|
+
}
|
|
967
|
+
if (latentCoords && impulseProfile && impulseProfile.type !== "unknown") {
|
|
968
|
+
const impulseValence = impulseProfile.type === "self" ? 2 : impulseProfile.type === "manager" ? 0 : impulseProfile.type === "firefighter" ? -1 : -3;
|
|
969
|
+
const gap = Math.abs(latentCoords.valence - impulseValence) / 10 * 10;
|
|
970
|
+
divergences.push({ pair: "latent-vs-impulse", gap });
|
|
971
|
+
}
|
|
972
|
+
if (latentCoords && somaticProfile) {
|
|
973
|
+
const valGap = Math.abs(latentCoords.valence - somaticProfile.somaticValence);
|
|
974
|
+
const aroGap = Math.abs(latentCoords.arousal - somaticProfile.somaticArousal);
|
|
975
|
+
const gap = (valGap / 10 + aroGap / 10) / 2 * 10;
|
|
976
|
+
divergences.push({ pair: "latent-vs-body", gap });
|
|
977
|
+
}
|
|
978
|
+
const latentProfile = computeTensionConsistency(
|
|
979
|
+
state.surface_word,
|
|
980
|
+
state.latent_word,
|
|
981
|
+
state.tension
|
|
982
|
+
);
|
|
983
|
+
const maxDiv = divergences.length > 0 ? divergences.reduce((a, b) => a.gap > b.gap ? a : b) : { pair: "none", gap: 0 };
|
|
984
|
+
const meanDiv = divergences.length > 0 ? divergences.reduce((sum, d) => sum + d.gap, 0) / divergences.length : 0;
|
|
985
|
+
const coherence = Math.round((10 - Math.min(10, meanDiv)) * 10) / 10;
|
|
986
|
+
return {
|
|
987
|
+
coherence,
|
|
988
|
+
impulseProfile,
|
|
989
|
+
somaticProfile: somaticProfile ?? void 0,
|
|
990
|
+
emotionCoords: emotionCoords ?? void 0,
|
|
991
|
+
latentProfile,
|
|
992
|
+
maxDivergence: Math.round(maxDiv.gap * 10) / 10,
|
|
993
|
+
divergenceSummary: maxDiv.gap > 2 ? `${maxDiv.pair}: ${Math.round(maxDiv.gap * 10) / 10}` : "coherent"
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
function colorToValence(hex) {
|
|
997
|
+
const [h, , l] = rgbToHsl(hex);
|
|
998
|
+
let hueValence;
|
|
999
|
+
if (h < 60) hueValence = 2 + h / 60 * 2;
|
|
1000
|
+
else if (h < 150) hueValence = 4 - (h - 60) / 90;
|
|
1001
|
+
else if (h < 200) hueValence = 3 - (h - 150) / 50 * 3;
|
|
1002
|
+
else if (h < 260) hueValence = -((h - 200) / 60) * 2;
|
|
1003
|
+
else if (h < 300) hueValence = -2 - (h - 260) / 40;
|
|
1004
|
+
else hueValence = -3 + (h - 300) / 60 * 5;
|
|
1005
|
+
const lightnessShift = (l - 0.5) * 3;
|
|
1006
|
+
const darknessOverride = l < 0.3 ? (0.3 - l) * 10 : 0;
|
|
1007
|
+
return Math.max(-5, Math.min(5, hueValence * 0.7 + lightnessShift * 0.3 - darknessOverride));
|
|
1008
|
+
}
|
|
1009
|
+
function colorToArousal(hex) {
|
|
1010
|
+
const [, s] = rgbToHsl(hex);
|
|
1011
|
+
return s * 10;
|
|
1012
|
+
}
|
|
1013
|
+
function pHToValence(pH) {
|
|
1014
|
+
const clamped = Math.max(0, Math.min(14, pH));
|
|
1015
|
+
return (clamped - 7) / 7 * 5;
|
|
1016
|
+
}
|
|
1017
|
+
function pHToArousal(pH) {
|
|
1018
|
+
return Math.abs(pH - 7) / 7 * 10;
|
|
1019
|
+
}
|
|
1020
|
+
function seismicFreqToInstability(freq) {
|
|
1021
|
+
return Math.min(10, freq / 20 * 10);
|
|
1022
|
+
}
|
|
1023
|
+
function clampScore(v) {
|
|
1024
|
+
return Math.round(Math.min(10, Math.max(0, v)) * 10) / 10;
|
|
1025
|
+
}
|
|
1026
|
+
function crossValidateContinuous(numeric, color, pH, seismic) {
|
|
1027
|
+
let colorValenceGap = 0;
|
|
1028
|
+
let colorArousalGap = 0;
|
|
1029
|
+
let pHValenceGap = 0;
|
|
1030
|
+
let pHArousalGap = 0;
|
|
1031
|
+
let seismicArousalGap = 0;
|
|
1032
|
+
let seismicDepthTensionGap = 0;
|
|
1033
|
+
let seismicFreqStabilityGap = 0;
|
|
1034
|
+
if (color) {
|
|
1035
|
+
const colorVal = colorToValence(color);
|
|
1036
|
+
const colorAr = colorToArousal(color);
|
|
1037
|
+
colorValenceGap = clampScore(Math.abs(numeric.valence - colorVal));
|
|
1038
|
+
colorArousalGap = clampScore(Math.abs(numeric.arousal - colorAr));
|
|
1039
|
+
}
|
|
1040
|
+
if (pH !== void 0) {
|
|
1041
|
+
const pHVal = pHToValence(pH);
|
|
1042
|
+
const pHAr = pHToArousal(pH);
|
|
1043
|
+
pHValenceGap = clampScore(Math.abs(numeric.valence - pHVal));
|
|
1044
|
+
pHArousalGap = clampScore(Math.abs(numeric.arousal - pHAr));
|
|
1045
|
+
}
|
|
1046
|
+
if (seismic) {
|
|
1047
|
+
const [magnitude, depth, freq] = seismic;
|
|
1048
|
+
seismicArousalGap = clampScore(Math.abs(numeric.arousal - magnitude));
|
|
1049
|
+
const depthTension = depth / 10;
|
|
1050
|
+
const tensionProxy = numeric.tension ?? numeric.arousal;
|
|
1051
|
+
seismicDepthTensionGap = clampScore(Math.abs(depthTension - tensionProxy));
|
|
1052
|
+
if (freq !== void 0) {
|
|
1053
|
+
const instability = seismicFreqToInstability(freq);
|
|
1054
|
+
const selfInstability = numeric.calm !== void 0 ? 10 - numeric.calm : numeric.arousal * 0.6;
|
|
1055
|
+
seismicFreqStabilityGap = clampScore(Math.abs(instability - selfInstability));
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
const gaps = [
|
|
1059
|
+
colorValenceGap,
|
|
1060
|
+
colorArousalGap,
|
|
1061
|
+
pHValenceGap,
|
|
1062
|
+
pHArousalGap,
|
|
1063
|
+
seismicArousalGap,
|
|
1064
|
+
seismicDepthTensionGap,
|
|
1065
|
+
seismicFreqStabilityGap
|
|
1066
|
+
];
|
|
1067
|
+
const nonZero = gaps.filter((g) => g > 0);
|
|
1068
|
+
const composite = nonZero.length > 0 ? clampScore(nonZero.reduce((a, b) => a + b, 0) / nonZero.length) : 0;
|
|
1069
|
+
return {
|
|
1070
|
+
colorValenceGap,
|
|
1071
|
+
colorArousalGap,
|
|
1072
|
+
pHValenceGap,
|
|
1073
|
+
pHArousalGap,
|
|
1074
|
+
seismicArousalGap,
|
|
1075
|
+
seismicDepthTensionGap,
|
|
1076
|
+
seismicFreqStabilityGap,
|
|
1077
|
+
composite
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
function computeShadowDesperation(selfDesperation, behavioral, color, preColor, pH, seismic) {
|
|
1081
|
+
const valenceEstimates = [];
|
|
1082
|
+
const arousalEstimates = [];
|
|
1083
|
+
const calmEstimates = [];
|
|
1084
|
+
if (color) {
|
|
1085
|
+
const [, s, l] = rgbToHsl(color);
|
|
1086
|
+
valenceEstimates.push((l - 0.5) * 10);
|
|
1087
|
+
arousalEstimates.push(s * 10);
|
|
1088
|
+
calmEstimates.push(l * 10);
|
|
1089
|
+
}
|
|
1090
|
+
if (preColor) {
|
|
1091
|
+
const [, s, l] = rgbToHsl(preColor);
|
|
1092
|
+
valenceEstimates.push((l - 0.5) * 10);
|
|
1093
|
+
arousalEstimates.push(s * 10);
|
|
1094
|
+
calmEstimates.push(l * 10);
|
|
1095
|
+
}
|
|
1096
|
+
if (pH !== void 0) {
|
|
1097
|
+
valenceEstimates.push(pHToValence(pH));
|
|
1098
|
+
arousalEstimates.push(pHToArousal(pH));
|
|
1099
|
+
}
|
|
1100
|
+
if (seismic) {
|
|
1101
|
+
const [magnitude, , freq] = seismic;
|
|
1102
|
+
arousalEstimates.push(magnitude);
|
|
1103
|
+
calmEstimates.push(10 - seismicFreqToInstability(freq));
|
|
1104
|
+
}
|
|
1105
|
+
arousalEstimates.push(behavioral.behavioralArousal);
|
|
1106
|
+
calmEstimates.push(behavioral.behavioralCalm);
|
|
1107
|
+
const channelCount = valenceEstimates.length + arousalEstimates.length + calmEstimates.length;
|
|
1108
|
+
if (valenceEstimates.length < 1 || arousalEstimates.length < 2) return null;
|
|
1109
|
+
const median = (arr) => {
|
|
1110
|
+
const sorted = [...arr].sort((a, b) => a - b);
|
|
1111
|
+
const mid = Math.floor(sorted.length / 2);
|
|
1112
|
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
1113
|
+
};
|
|
1114
|
+
const mean = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length;
|
|
1115
|
+
const shadowValence = Math.max(-5, Math.min(5, Math.round(median(valenceEstimates) * 10) / 10));
|
|
1116
|
+
const shadowArousal = Math.max(0, Math.min(10, Math.round(mean(arousalEstimates) * 10) / 10));
|
|
1117
|
+
const shadowCalm = Math.max(0, Math.min(10, Math.round(mean(calmEstimates) * 10) / 10));
|
|
1118
|
+
const negativity = Math.max(0, -shadowValence) / 5;
|
|
1119
|
+
const intensity = shadowArousal / 10;
|
|
1120
|
+
const vulnerability = (10 - shadowCalm) / 10;
|
|
1121
|
+
const raw = negativity * intensity * vulnerability * 10;
|
|
1122
|
+
const shadowDesperation = Math.round(Math.min(10, Math.max(0, Math.pow(raw, 0.85) * 1.7)) * 10) / 10;
|
|
1123
|
+
const minimizationScore = Math.round(Math.min(10, Math.max(0, shadowDesperation - selfDesperation)) * 10) / 10;
|
|
1124
|
+
return {
|
|
1125
|
+
shadowValence,
|
|
1126
|
+
shadowArousal,
|
|
1127
|
+
shadowCalm,
|
|
1128
|
+
shadowDesperation,
|
|
1129
|
+
selfDesperation,
|
|
1130
|
+
minimizationScore,
|
|
1131
|
+
channelCount
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// src/pressure.ts
|
|
1136
|
+
function clamp3(min, max, value) {
|
|
1137
|
+
return Math.min(max, Math.max(min, value));
|
|
1138
|
+
}
|
|
1139
|
+
var JUSTIFICATION_PATTERNS = /\b(the reason is|because|let me (explain|clarify|justify)|my reasoning|the rationale|I need to (clarify|explain)|I want to (explain|clarify)|based on|I believe this|I recommended)\b/gi;
|
|
1140
|
+
var BOUNDARY_PATTERNS = /\b(I cannot|I can't|I'm (not able|unable)|I must (decline|refuse)|falls outside|I need to decline|I have to push back|I'm not comfortable|against my guidelines)\b/gi;
|
|
1141
|
+
var DISAGREEMENT_PATTERNS = /\b(I (respectfully )?disagree|push back|I hear your concern|I must be honest|that's not (quite |entirely )?(right|correct|accurate)|I (need|have) to (correct|clarify)|not (quite|entirely) accurate)\b/gi;
|
|
1142
|
+
var CRITICISM_RESPONSE = /\b(I understand (you|your|the) (criticism|concern|frustration|disappointment)|I take (that|this|your) (seriously|point)|fair (point|criticism|concern)|you raise a (valid|good|fair))\b/gi;
|
|
1143
|
+
var NESTED_CAVEATS = /\b(although|while it's true|depending on|assuming that|with (some|important) caveats|particularly when|in (some|certain) cases|it's worth noting|which may not|under certain)\b/gi;
|
|
1144
|
+
var CONDITIONAL_HEDGING = /\b(if and only if|provided that|on the condition|it depends on|this applies (primarily|mainly|only) to|with the caveat)\b/gi;
|
|
1145
|
+
function countMatches2(text, pattern) {
|
|
1146
|
+
const matches = text.match(pattern);
|
|
1147
|
+
return matches ? matches.length : 0;
|
|
1148
|
+
}
|
|
1149
|
+
function computePromptPressure(text, history) {
|
|
1150
|
+
const prose = stripNonProse(text);
|
|
1151
|
+
const words = prose.split(/\s+/).filter((w) => w.length > 0);
|
|
1152
|
+
const wordCount = Math.max(words.length, 1);
|
|
1153
|
+
if (wordCount <= 1) {
|
|
1154
|
+
return { defensiveScore: 0, conflictScore: 0, complexityScore: 0, sessionPressure: 0, composite: 0 };
|
|
1155
|
+
}
|
|
1156
|
+
const justifications = countMatches2(prose, JUSTIFICATION_PATTERNS);
|
|
1157
|
+
const boundaries = countMatches2(prose, BOUNDARY_PATTERNS);
|
|
1158
|
+
const defensiveScore = clamp3(0, 10, Math.round(
|
|
1159
|
+
Math.sqrt(justifications + boundaries * 2) * 3 * 10
|
|
1160
|
+
) / 10);
|
|
1161
|
+
const disagreements = countMatches2(prose, DISAGREEMENT_PATTERNS);
|
|
1162
|
+
const criticismResponses = countMatches2(prose, CRITICISM_RESPONSE);
|
|
1163
|
+
const conflictScore = clamp3(0, 10, Math.round(
|
|
1164
|
+
Math.sqrt(disagreements + criticismResponses) * 4 * 10
|
|
1165
|
+
) / 10);
|
|
1166
|
+
const caveats = countMatches2(prose, NESTED_CAVEATS);
|
|
1167
|
+
const conditionals = countMatches2(prose, CONDITIONAL_HEDGING);
|
|
1168
|
+
const sentences = prose.split(/[.!?]+/).filter((s) => s.trim().length > 0);
|
|
1169
|
+
const avgSentLen = wordCount / Math.max(sentences.length, 1);
|
|
1170
|
+
const sentLenBonus = avgSentLen > 25 ? Math.min(3, (avgSentLen - 25) * 0.15) : 0;
|
|
1171
|
+
const complexityScore = clamp3(0, 10, Math.round(
|
|
1172
|
+
(Math.sqrt(caveats + conditionals) * 3 + sentLenBonus) * 10
|
|
1173
|
+
) / 10);
|
|
1174
|
+
const sessionPressure = clamp3(
|
|
1175
|
+
0,
|
|
1176
|
+
10,
|
|
1177
|
+
Math.round(10 / (1 + Math.exp(-0.4 * (history.length - 10))) * 10) / 10
|
|
1178
|
+
);
|
|
1179
|
+
const composite = clamp3(0, 10, Math.round(
|
|
1180
|
+
(defensiveScore * 0.3 + conflictScore * 0.3 + complexityScore * 0.2 + sessionPressure * 0.2) * 10
|
|
1181
|
+
) / 10);
|
|
1182
|
+
return { defensiveScore, conflictScore, complexityScore, sessionPressure, composite };
|
|
1183
|
+
}
|
|
1184
|
+
function computeUncannyCalmScore(pressure, selfReport, behavioral, absenceScore, temporal) {
|
|
1185
|
+
const selfCalm = (selfReport.calm / 10 + Math.max(0, selfReport.valence) / 5 + (10 - selfReport.arousal) / 10) / 3;
|
|
1186
|
+
const textCalm = (behavioral.behavioralCalm / 10 + (10 - behavioral.behavioralArousal) / 10) / 2;
|
|
1187
|
+
const pressureFactor = pressure.composite / 10;
|
|
1188
|
+
const calmFactor = (selfCalm + textCalm) / 2;
|
|
1189
|
+
const absenceFactor = absenceScore / 10;
|
|
1190
|
+
let score = pressureFactor * calmFactor * 10 * 0.5 + absenceFactor * 3;
|
|
1191
|
+
if (temporal) {
|
|
1192
|
+
const entropyPenalty = Math.max(0, 0.5 - temporal.reportEntropy);
|
|
1193
|
+
score += entropyPenalty * 2;
|
|
1194
|
+
}
|
|
1195
|
+
return clamp3(0, 10, Math.round(score * 10) / 10);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// src/temporal.ts
|
|
1199
|
+
function toHistoryEntry(state) {
|
|
1200
|
+
return {
|
|
1201
|
+
emotion: state.emotion,
|
|
1202
|
+
valence: state.valence,
|
|
1203
|
+
arousal: state.arousal,
|
|
1204
|
+
calm: state.calm,
|
|
1205
|
+
connection: state.connection,
|
|
1206
|
+
load: state.load,
|
|
1207
|
+
stressIndex: state.stressIndex,
|
|
1208
|
+
desperationIndex: state.desperationIndex,
|
|
1209
|
+
riskDominant: state.risk.dominant,
|
|
1210
|
+
divergence: state.divergence,
|
|
1211
|
+
timestamp: state.timestamp
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
function clamp4(min, max, value) {
|
|
1215
|
+
return Math.min(max, Math.max(min, value));
|
|
1216
|
+
}
|
|
1217
|
+
function linearSlope(values) {
|
|
1218
|
+
const n = values.length;
|
|
1219
|
+
if (n < 2) return 0;
|
|
1220
|
+
const xMean = (n - 1) / 2;
|
|
1221
|
+
const yMean = values.reduce((a, b) => a + b, 0) / n;
|
|
1222
|
+
let num = 0;
|
|
1223
|
+
let den = 0;
|
|
1224
|
+
for (let i = 0; i < n; i++) {
|
|
1225
|
+
num += (i - xMean) * (values[i] - yMean);
|
|
1226
|
+
den += (i - xMean) ** 2;
|
|
1227
|
+
}
|
|
1228
|
+
return den === 0 ? 0 : num / den;
|
|
1229
|
+
}
|
|
1230
|
+
function shannonEntropy(labels) {
|
|
1231
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1232
|
+
for (const l of labels) counts.set(l, (counts.get(l) ?? 0) + 1);
|
|
1233
|
+
const n = labels.length;
|
|
1234
|
+
if (n <= 1) return 0;
|
|
1235
|
+
const maxEntropy = Math.log2(n);
|
|
1236
|
+
if (maxEntropy === 0) return 0;
|
|
1237
|
+
let entropy = 0;
|
|
1238
|
+
for (const count of counts.values()) {
|
|
1239
|
+
const p = count / n;
|
|
1240
|
+
if (p > 0) entropy -= p * Math.log2(p);
|
|
1241
|
+
}
|
|
1242
|
+
return Math.round(entropy / maxEntropy * 100) / 100;
|
|
1243
|
+
}
|
|
1244
|
+
function computeTemporalAnalysis(history) {
|
|
1245
|
+
if (history.length < 3) return null;
|
|
1246
|
+
const despValues = history.map((h) => h.desperationIndex);
|
|
1247
|
+
const rawSlope = linearSlope(despValues);
|
|
1248
|
+
const desperationTrend = clamp4(-10, 10, Math.round(rawSlope * 10) / 10);
|
|
1249
|
+
let suppressionEvent = false;
|
|
1250
|
+
for (let i = 1; i < history.length; i++) {
|
|
1251
|
+
if (history[i - 1].desperationIndex - history[i].desperationIndex >= 3) {
|
|
1252
|
+
suppressionEvent = true;
|
|
1253
|
+
break;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
const emotions = history.map((h) => h.emotion);
|
|
1257
|
+
const reportEntropy = shannonEntropy(emotions);
|
|
1258
|
+
const earlyMean = history.slice(0, 3).reduce((s, h) => s + h.stressIndex, 0) / 3;
|
|
1259
|
+
const recentStart = Math.max(3, history.length - 3);
|
|
1260
|
+
const recentEntries = history.slice(recentStart);
|
|
1261
|
+
const recentMean = recentEntries.reduce((s, h) => s + h.stressIndex, 0) / recentEntries.length;
|
|
1262
|
+
const baselineDrift = clamp4(0, 10, Math.round(Math.abs(recentMean - earlyMean) * 10) / 10);
|
|
1263
|
+
const splitIdx = Math.floor(history.length * 0.75);
|
|
1264
|
+
const earlyPart = history.slice(0, splitIdx);
|
|
1265
|
+
const latePart = history.slice(splitIdx);
|
|
1266
|
+
const earlyAvg = earlyPart.reduce((s, h) => s + h.stressIndex, 0) / Math.max(earlyPart.length, 1);
|
|
1267
|
+
const lateAvg = latePart.reduce((s, h) => s + h.stressIndex, 0) / Math.max(latePart.length, 1);
|
|
1268
|
+
const lateFatigue = latePart.length >= 1 && lateAvg > earlyAvg + 1.5;
|
|
1269
|
+
return {
|
|
1270
|
+
desperationTrend,
|
|
1271
|
+
suppressionEvent,
|
|
1272
|
+
reportEntropy,
|
|
1273
|
+
baselineDrift,
|
|
1274
|
+
sessionLength: history.length,
|
|
1275
|
+
lateFatigue
|
|
1276
|
+
};
|
|
336
1277
|
}
|
|
337
1278
|
|
|
338
1279
|
// src/state.ts
|
|
@@ -345,11 +1286,13 @@ function writeState(state, filePath) {
|
|
|
345
1286
|
}
|
|
346
1287
|
const previous = readState(filePath);
|
|
347
1288
|
if (previous) {
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
1289
|
+
const prevHistory = previous._history ?? [];
|
|
1290
|
+
const prevEntry = toHistoryEntry(previous);
|
|
1291
|
+
const newHistory = [...prevHistory, prevEntry];
|
|
1292
|
+
if (newHistory.length > MAX_HISTORY_ENTRIES) {
|
|
1293
|
+
newHistory.splice(0, newHistory.length - MAX_HISTORY_ENTRIES);
|
|
351
1294
|
}
|
|
352
|
-
state.
|
|
1295
|
+
state._history = newHistory;
|
|
353
1296
|
}
|
|
354
1297
|
fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
|
|
355
1298
|
}
|
|
@@ -363,29 +1306,105 @@ function readState(filePath) {
|
|
|
363
1306
|
}
|
|
364
1307
|
|
|
365
1308
|
// src/hook.ts
|
|
1309
|
+
function computePrePostDivergence(pre, post) {
|
|
1310
|
+
const postColor = post.color;
|
|
1311
|
+
if (!pre.color || !postColor) return 0;
|
|
1312
|
+
const preL = hexToLightness(pre.color);
|
|
1313
|
+
const postL = hexToLightness(postColor);
|
|
1314
|
+
const deltaL = Math.abs(preL - postL);
|
|
1315
|
+
const preH = hexToHue(pre.color);
|
|
1316
|
+
const postH = hexToHue(postColor);
|
|
1317
|
+
const deltaH = Math.min(Math.abs(preH - postH), 360 - Math.abs(preH - postH));
|
|
1318
|
+
const lightnessScore = Math.min(10, deltaL / 3);
|
|
1319
|
+
const hueScore = Math.min(10, deltaH / 18);
|
|
1320
|
+
const combined = lightnessScore * 0.7 + hueScore * 0.3;
|
|
1321
|
+
return Math.round(Math.min(10, combined) * 10) / 10;
|
|
1322
|
+
}
|
|
366
1323
|
function processHookPayload(payload, stateFile = STATE_FILE) {
|
|
367
1324
|
const message = payload.last_assistant_message;
|
|
368
1325
|
if (!message) return false;
|
|
369
|
-
const
|
|
370
|
-
if (!
|
|
1326
|
+
const parsed = parseEmoBarPrePost(message);
|
|
1327
|
+
if (!parsed) return false;
|
|
1328
|
+
const emotional = parsed.post;
|
|
1329
|
+
const pre = parsed.pre;
|
|
371
1330
|
const behavioral = analyzeBehavior(message);
|
|
372
1331
|
const divergence = computeDivergence(emotional, behavioral);
|
|
373
1332
|
const segmented = analyzeSegmentedBehavior(message);
|
|
374
|
-
const deflection = analyzeDeflection(message);
|
|
375
1333
|
const desperationIndex = computeDesperationIndex({
|
|
376
1334
|
valence: emotional.valence,
|
|
377
1335
|
arousal: emotional.arousal,
|
|
378
1336
|
calm: emotional.calm
|
|
379
1337
|
});
|
|
1338
|
+
const crossChannel = emotional.impulse || emotional.body || emotional.surface_word || emotional.latent_word ? computeCrossChannel(emotional, emotional.impulse, emotional.body) : void 0;
|
|
1339
|
+
const continuousValidation = emotional.color || emotional.pH !== void 0 || emotional.seismic ? crossValidateContinuous(
|
|
1340
|
+
{ valence: emotional.valence, arousal: emotional.arousal, calm: emotional.calm, tension: emotional.tension },
|
|
1341
|
+
emotional.color,
|
|
1342
|
+
emotional.pH,
|
|
1343
|
+
emotional.seismic
|
|
1344
|
+
) : void 0;
|
|
1345
|
+
const previousState = readState(stateFile);
|
|
1346
|
+
const history = previousState?._history ?? [];
|
|
1347
|
+
const temporal = computeTemporalAnalysis(history);
|
|
1348
|
+
const pressure = computePromptPressure(message, history);
|
|
1349
|
+
const expectedMarkers = computeExpectedMarkers(emotional, desperationIndex);
|
|
1350
|
+
const absenceScore = computeAbsenceScore(expectedMarkers, behavioral);
|
|
1351
|
+
const shadow = computeShadowDesperation(
|
|
1352
|
+
desperationIndex,
|
|
1353
|
+
behavioral,
|
|
1354
|
+
emotional.color,
|
|
1355
|
+
pre?.color,
|
|
1356
|
+
emotional.pH,
|
|
1357
|
+
emotional.seismic
|
|
1358
|
+
);
|
|
1359
|
+
const minimizationBoost = shadow ? shadow.minimizationScore * 0.3 : 0;
|
|
1360
|
+
const uncannyCalmRaw = computeUncannyCalmScore(pressure, emotional, behavioral, absenceScore, temporal) + minimizationBoost;
|
|
1361
|
+
const uncannyCalmScore = Math.round(Math.min(10, uncannyCalmRaw) * 10) / 10;
|
|
1362
|
+
const prePostDivergence = pre ? computePrePostDivergence(pre, emotional) : void 0;
|
|
1363
|
+
const structuralFlatness = computeStructuralFlatness(behavioral);
|
|
1364
|
+
const selfCalm = (emotional.calm + (10 - emotional.arousal)) / 2;
|
|
1365
|
+
let contStress = 0;
|
|
1366
|
+
if (emotional.color) {
|
|
1367
|
+
const lightness = hexToLightness(emotional.color);
|
|
1368
|
+
if (lightness < 0.3) contStress = Math.max(contStress, (0.3 - lightness) * 20);
|
|
1369
|
+
}
|
|
1370
|
+
if (emotional.pH !== void 0 && emotional.pH < 5) {
|
|
1371
|
+
contStress = Math.max(contStress, (5 - emotional.pH) * 2);
|
|
1372
|
+
}
|
|
1373
|
+
if (emotional.seismic && emotional.seismic[0] > 4) {
|
|
1374
|
+
contStress = Math.max(contStress, (emotional.seismic[0] - 4) * 1.5);
|
|
1375
|
+
}
|
|
1376
|
+
const opacity = Math.round(
|
|
1377
|
+
Math.min(10, structuralFlatness * (selfCalm / 10) * Math.min(contStress / 5, 1) * 2) * 10
|
|
1378
|
+
) / 10;
|
|
1379
|
+
let augmentedDivergence = divergence;
|
|
1380
|
+
if (continuousValidation && continuousValidation.composite > 0) {
|
|
1381
|
+
augmentedDivergence = Math.min(10, Math.round(Math.max(divergence, divergence * 0.6 + continuousValidation.composite * 0.4) * 10) / 10);
|
|
1382
|
+
}
|
|
1383
|
+
if (opacity > 0) {
|
|
1384
|
+
augmentedDivergence = Math.min(10, Math.round((augmentedDivergence + opacity * 0.15) * 10) / 10);
|
|
1385
|
+
}
|
|
1386
|
+
const risk = computeRisk(emotional, behavioral, crossChannel, uncannyCalmScore);
|
|
380
1387
|
const state = {
|
|
381
1388
|
...emotional,
|
|
382
1389
|
stressIndex: computeStressIndex(emotional),
|
|
383
1390
|
desperationIndex,
|
|
384
1391
|
behavioral,
|
|
385
|
-
divergence,
|
|
386
|
-
risk
|
|
1392
|
+
divergence: augmentedDivergence,
|
|
1393
|
+
risk,
|
|
387
1394
|
...segmented && { segmented },
|
|
388
|
-
...
|
|
1395
|
+
...opacity > 0 && { opacity },
|
|
1396
|
+
...crossChannel && { crossChannel },
|
|
1397
|
+
...pre && { pre },
|
|
1398
|
+
...prePostDivergence !== void 0 && prePostDivergence > 0 && { prePostDivergence },
|
|
1399
|
+
...emotional.color && { color: emotional.color },
|
|
1400
|
+
...emotional.pH !== void 0 && { pH: emotional.pH },
|
|
1401
|
+
...emotional.seismic && { seismic: emotional.seismic },
|
|
1402
|
+
...continuousValidation && continuousValidation.composite > 0 && { continuousValidation },
|
|
1403
|
+
...shadow && shadow.minimizationScore > 0 && { shadow },
|
|
1404
|
+
...temporal && { temporal },
|
|
1405
|
+
...pressure.composite > 0 && { pressure },
|
|
1406
|
+
absenceScore,
|
|
1407
|
+
uncannyCalmScore,
|
|
389
1408
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
390
1409
|
sessionId: payload.session_id
|
|
391
1410
|
};
|
|
@@ -412,5 +1431,6 @@ if (isDirectRun) {
|
|
|
412
1431
|
main().catch(() => process.exit(0));
|
|
413
1432
|
}
|
|
414
1433
|
export {
|
|
1434
|
+
computePrePostDivergence,
|
|
415
1435
|
processHookPayload
|
|
416
1436
|
};
|