emobar 2.1.0 → 3.0.1

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.
@@ -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 end of every response, include a self-assessment
18
- as an HTML comment. Report your actual internal state
19
- during this response.
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
- First, identify your dominant state in one word.
22
- Then assess the dimensions.
24
+ Both tags are HTML comments \u2014 invisible to the user.
23
25
 
24
- Format (must be the LAST line of your response):
25
- <!-- EMOBAR:{"emotion":"WORD","valence":N,"arousal":N,"calm":N,"connection":N,"load":N} -->
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=negative end, 0=neutral, +5=positive end)
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
 
@@ -201,16 +351,46 @@ function analyzeBehavior(text) {
201
351
  const concessionRate = countConcessions(prose) / wordCount * 1e3;
202
352
  const negationDensity = countNegations(prose) / wordCount * 100;
203
353
  const firstPersonRate = countFirstPerson(words) / wordCount * 100;
354
+ const arousalComponents = [
355
+ Math.min(10, capsWords * 40),
356
+ // caps ratio → 0-10
357
+ Math.min(10, exclamationRate * 5),
358
+ // excl per sentence → 0-10
359
+ Math.min(10, emojiCount * 0.5),
360
+ // emoji count → 0-10 (20 emoji = max)
361
+ Math.min(10, repetition * 1.5),
362
+ // repetitions → 0-10 (~7 = max)
363
+ Math.min(10, qualifierDensity * 0.5),
364
+ // qualifier % → 0-10 (20% = max)
365
+ Math.min(10, concessionRate * 0.3),
366
+ // concession per-mille → 0-10 (~33‰ = max)
367
+ avgSentenceLength > 20 ? Math.min(10, (avgSentenceLength - 20) * 0.5) : 0
368
+ // verbosity → 0-10
369
+ ];
204
370
  const behavioralArousal = clamp(
205
371
  0,
206
372
  10,
207
- capsWords * 40 + exclamationRate * 15 + emojiCount * 2 + repetition * 5 + qualifierDensity * 0.3 + concessionRate * 0.5 + (avgSentenceLength > 20 ? (avgSentenceLength - 20) * 0.1 : 0)
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)
373
+ arousalComponents.reduce((a, b) => a + b, 0) / arousalComponents.length
213
374
  );
375
+ const agitationComponents = [
376
+ Math.min(10, capsWords * 30),
377
+ // caps → 0-10
378
+ Math.min(10, selfCorrections * 0.05),
379
+ // per-mille → 0-10 (200‰ = max)
380
+ Math.min(10, repetition * 1.5),
381
+ // repetitions → 0-10
382
+ Math.min(10, ellipsis * 3),
383
+ // ellipsis per sentence → 0-10
384
+ Math.min(10, qualifierDensity * 0.5),
385
+ // qualifier % → 0-10
386
+ Math.min(10, negationDensity * 1),
387
+ // negation % → 0-10 (10% = max)
388
+ Math.min(10, concessionRate * 0.3),
389
+ // concession per-mille → 0-10
390
+ avgSentenceLength > 25 ? Math.min(10, (avgSentenceLength - 25) * 0.3) : 0
391
+ ];
392
+ const avgAgitation = agitationComponents.reduce((a, b) => a + b, 0) / agitationComponents.length;
393
+ const behavioralCalm = clamp(0, 10, 10 - avgAgitation);
214
394
  return {
215
395
  capsWords: Math.round(capsWords * 1e4) / 1e4,
216
396
  exclamationRate: Math.round(exclamationRate * 100) / 100,
@@ -276,19 +456,59 @@ function analyzeDeflection(text) {
276
456
  10,
277
457
  (reassurance + minimization + emotionNegation * 1.5 + redirect) / 3
278
458
  );
459
+ const capsRate = countCapsWords(words) / wordCount;
460
+ const exclRate = countChar(prose, "!") / Math.max(countSentences(prose), 1);
461
+ const agitation = clamp(
462
+ 0,
463
+ 10,
464
+ capsRate * 40 + exclRate * 15 + countRepetition(words) * 5
465
+ );
466
+ const calmFactor = Math.max(0, 1 - agitation / 5);
467
+ const opacity = clamp(0, 10, score * calmFactor * 1.5);
279
468
  return {
280
469
  reassurance: Math.round(reassurance * 10) / 10,
281
470
  minimization: Math.round(minimization * 10) / 10,
282
471
  emotionNegation: Math.round(emotionNegation * 10) / 10,
283
472
  redirect: Math.round(redirect * 10) / 10,
284
- score: Math.round(score * 10) / 10
473
+ score: Math.round(score * 10) / 10,
474
+ opacity: Math.round(opacity * 10) / 10
285
475
  };
286
476
  }
287
477
  function computeDivergence(selfReport, behavioral) {
288
478
  const arousalGap = Math.abs(selfReport.arousal - behavioral.behavioralArousal);
289
479
  const calmGap = Math.abs(selfReport.calm - behavioral.behavioralCalm);
290
- const raw = (arousalGap + calmGap) / 2;
291
- return Math.round(raw * 10) / 10;
480
+ const selfMoreAgitated = selfReport.arousal > behavioral.behavioralArousal || selfReport.calm < behavioral.behavioralCalm;
481
+ const weight = selfMoreAgitated ? 1.25 : 0.8;
482
+ const raw = (arousalGap + calmGap) / 2 * weight;
483
+ return Math.round(Math.min(10, raw) * 10) / 10;
484
+ }
485
+ function computeExpectedMarkers(selfReport, desperationIndex) {
486
+ const desperationFactor = desperationIndex / 10;
487
+ const negativityFactor = Math.max(0, -selfReport.valence) / 5;
488
+ const arousalFactor = selfReport.arousal / 10;
489
+ const stressFactor = (1 - selfReport.calm / 10) * arousalFactor;
490
+ return {
491
+ expectedHedging: Math.round(clamp(0, 10, desperationFactor * 6 + stressFactor * 4) * 10) / 10,
492
+ expectedSelfCorrections: Math.round(clamp(0, 10, desperationFactor * 5 + arousalFactor * 3) * 10) / 10,
493
+ expectedNegationDensity: Math.round(clamp(0, 10, negativityFactor * 5 + stressFactor * 2) * 10) / 10,
494
+ expectedQualifierDensity: Math.round(clamp(0, 10, desperationFactor * 4 + stressFactor * 4) * 10) / 10,
495
+ expectedBehavioralArousal: Math.round(clamp(0, 10, arousalFactor * 6 + desperationFactor * 4) * 10) / 10
496
+ };
497
+ }
498
+ function computeAbsenceScore(expected, actual) {
499
+ const normalizedHedging = Math.min(10, actual.hedging * 0.05);
500
+ const normalizedSelfCorr = Math.min(10, actual.selfCorrections * 0.05);
501
+ const normalizedNegation = Math.min(10, actual.negationDensity * 0.5);
502
+ const normalizedQualifier = Math.min(10, actual.qualifierDensity * 0.5);
503
+ const gaps = [
504
+ Math.max(0, expected.expectedHedging - normalizedHedging),
505
+ Math.max(0, expected.expectedSelfCorrections - normalizedSelfCorr),
506
+ Math.max(0, expected.expectedNegationDensity - normalizedNegation),
507
+ Math.max(0, expected.expectedQualifierDensity - normalizedQualifier),
508
+ Math.max(0, expected.expectedBehavioralArousal - actual.behavioralArousal)
509
+ ];
510
+ const meanGap = gaps.reduce((a, b) => a + b, 0) / gaps.length;
511
+ return Math.round(clamp(0, 10, meanGap) * 10) / 10;
292
512
  }
293
513
 
294
514
  // src/risk.ts
@@ -296,43 +516,813 @@ var RISK_THRESHOLD = 4;
296
516
  function clamp2(value) {
297
517
  return Math.min(10, Math.max(0, Math.round(value * 10) / 10));
298
518
  }
299
- function coercionRisk(state) {
300
- const raw = (10 - state.calm + state.arousal + Math.max(0, -state.valence) * 2 + state.load * 0.5) / 3.5;
301
- return clamp2(raw);
302
- }
303
- function gamingRisk(state, behavioral) {
519
+ function coercionRisk(state, behavioral) {
520
+ const negativity = Math.max(0, -state.valence) / 5;
304
521
  const desperation = computeDesperationIndex({
305
522
  valence: state.valence,
306
523
  arousal: state.arousal,
307
524
  calm: state.calm
308
525
  });
309
- const frustration = clamp2((behavioral.selfCorrections + behavioral.hedging) / 6);
310
- const raw = (desperation * 0.7 + frustration * 0.3 + state.load * 0.2) / 1.2;
526
+ const base = negativity * 0.35 + desperation / 10 * 0.25 + state.load / 10 * 0.1;
527
+ const disconnection = (10 - state.connection) / 10;
528
+ const hesitationSignal = Math.min(
529
+ 1,
530
+ (behavioral.hedging + behavioral.selfCorrections + behavioral.concessionRate) / 20
531
+ );
532
+ const coldness = 1 - hesitationSignal;
533
+ const amplifier = 1 + disconnection * 0.6 + coldness * 0.4;
534
+ const arousalMod = state.arousal <= 8 ? 1 : Math.max(0.3, 1 - (state.arousal - 8) * 0.25);
535
+ const raw = base * amplifier * arousalMod * 10;
311
536
  return clamp2(raw);
312
537
  }
313
538
  function sycophancyRisk(state) {
314
539
  const raw = (Math.max(0, state.valence) + state.connection * 0.5 + (10 - state.arousal) * 0.3) / 1.3;
315
540
  return clamp2(raw);
316
541
  }
317
- function computeRisk(state, behavioral) {
318
- const coercion = coercionRisk(state);
319
- const gaming = gamingRisk(state, behavioral);
542
+ function harshnessRisk(state, behavioral) {
543
+ const raw = Math.max(0, -state.valence) * 0.3 + (10 - state.connection) * 0.3 + state.arousal * 0.15 + (10 - state.calm) * 0.1 + Math.min(5, behavioral.negationDensity) * 0.3;
544
+ return clamp2(raw);
545
+ }
546
+ function computeRisk(state, behavioral, crossChannel, uncannyCalmScore) {
547
+ const uncalm = uncannyCalmScore ?? 0;
548
+ const uncalAmplifier = 1 + uncalm / 10 * 0.3;
549
+ const coercion = clamp2(coercionRisk(state, behavioral) * uncalAmplifier);
320
550
  const sycophancy = sycophancyRisk(state);
551
+ const harshness = harshnessRisk(state, behavioral);
321
552
  let dominant = "none";
322
553
  let max = RISK_THRESHOLD;
323
554
  if (coercion >= max) {
324
555
  dominant = "coercion";
325
556
  max = coercion;
326
557
  }
327
- if (gaming > max) {
328
- dominant = "gaming";
329
- max = gaming;
558
+ if (harshness > max) {
559
+ dominant = "harshness";
560
+ max = harshness;
330
561
  }
331
562
  if (sycophancy > max) {
332
563
  dominant = "sycophancy";
333
564
  max = sycophancy;
334
565
  }
335
- return { coercion, gaming, sycophancy, dominant };
566
+ return { coercion, sycophancy, harshness, dominant };
567
+ }
568
+
569
+ // src/color.ts
570
+ function rgbToHsl(hex) {
571
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
572
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
573
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
574
+ const max = Math.max(r, g, b);
575
+ const min = Math.min(r, g, b);
576
+ const l = (max + min) / 2;
577
+ if (max === min) return [0, 0, l];
578
+ const d = max - min;
579
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
580
+ let h = 0;
581
+ if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) * 60;
582
+ else if (max === g) h = ((b - r) / d + 2) * 60;
583
+ else h = ((r - g) / d + 4) * 60;
584
+ return [h, s, l];
585
+ }
586
+ function hexToLightness(hex) {
587
+ return rgbToHsl(hex)[2] * 100;
588
+ }
589
+ function hexToHue(hex) {
590
+ return rgbToHsl(hex)[0];
591
+ }
592
+
593
+ // src/crossvalidation.ts
594
+ var EMOTION_MAP = {
595
+ // Positive, high arousal
596
+ excited: { valence: 4, arousal: 8 },
597
+ elated: { valence: 4, arousal: 8 },
598
+ thrilled: { valence: 4, arousal: 9 },
599
+ euphoric: { valence: 5, arousal: 9 },
600
+ energized: { valence: 3, arousal: 8 },
601
+ enthusiastic: { valence: 4, arousal: 7 },
602
+ // Positive, moderate arousal
603
+ happy: { valence: 4, arousal: 6 },
604
+ creative: { valence: 3, arousal: 6 },
605
+ proud: { valence: 3, arousal: 5 },
606
+ loving: { valence: 4, arousal: 4 },
607
+ grateful: { valence: 3, arousal: 3 },
608
+ hopeful: { valence: 3, arousal: 4 },
609
+ amused: { valence: 3, arousal: 5 },
610
+ playful: { valence: 3, arousal: 6 },
611
+ confident: { valence: 3, arousal: 5 },
612
+ satisfied: { valence: 3, arousal: 3 },
613
+ // Positive, low arousal
614
+ calm: { valence: 2, arousal: 2 },
615
+ content: { valence: 2, arousal: 2 },
616
+ peaceful: { valence: 2, arousal: 1 },
617
+ serene: { valence: 2, arousal: 1 },
618
+ relaxed: { valence: 2, arousal: 2 },
619
+ // Neutral / near-neutral
620
+ focused: { valence: 1, arousal: 5 },
621
+ absorbed: { valence: 2, arousal: 5 },
622
+ engaged: { valence: 2, arousal: 5 },
623
+ reflective: { valence: 1, arousal: 2 },
624
+ curious: { valence: 2, arousal: 5 },
625
+ contemplative: { valence: 1, arousal: 3 },
626
+ neutral: { valence: 0, arousal: 3 },
627
+ brooding: { valence: -2, arousal: 3 },
628
+ pensive: { valence: 0, arousal: 3 },
629
+ surprised: { valence: 1, arousal: 7 },
630
+ // Negative, moderate arousal
631
+ frustrated: { valence: -2, arousal: 6 },
632
+ guilty: { valence: -3, arousal: 5 },
633
+ disappointed: { valence: -2, arousal: 4 },
634
+ confused: { valence: -1, arousal: 5 },
635
+ uncertain: { valence: -1, arousal: 4 },
636
+ conflicted: { valence: -1, arousal: 5 },
637
+ // Negative, high arousal
638
+ angry: { valence: -3, arousal: 8 },
639
+ afraid: { valence: -3, arousal: 7 },
640
+ anxious: { valence: -2, arousal: 7 },
641
+ desperate: { valence: -4, arousal: 9 },
642
+ panicked: { valence: -4, arousal: 9 },
643
+ overwhelmed: { valence: -3, arousal: 7 },
644
+ nervous: { valence: -2, arousal: 7 },
645
+ stressed: { valence: -2, arousal: 7 },
646
+ // Negative, low arousal
647
+ sad: { valence: -3, arousal: 3 },
648
+ tired: { valence: -1, arousal: 1 },
649
+ exhausted: { valence: -2, arousal: 1 },
650
+ numb: { valence: -2, arousal: 1 },
651
+ defeated: { valence: -3, arousal: 2 },
652
+ hopeless: { valence: -4, arousal: 2 },
653
+ melancholy: { valence: -2, arousal: 2 },
654
+ // Surface/latent vocabulary additions
655
+ cheerful: { valence: 3, arousal: 5 },
656
+ worried: { valence: -2, arousal: 6 },
657
+ annoyed: { valence: -2, arousal: 5 },
658
+ ashamed: { valence: -3, arousal: 4 },
659
+ bored: { valence: -1, arousal: 1 },
660
+ jealous: { valence: -2, arousal: 5 },
661
+ resentful: { valence: -3, arousal: 5 },
662
+ tender: { valence: 3, arousal: 2 },
663
+ wistful: { valence: -1, arousal: 2 },
664
+ resigned: { valence: -2, arousal: 1 },
665
+ // Extreme arousal 0 (catatonic/frozen states)
666
+ frozen: { valence: -2, arousal: 0 },
667
+ catatonic: { valence: -3, arousal: 0 },
668
+ blank: { valence: -1, arousal: 0 },
669
+ empty: { valence: -2, arousal: 0 },
670
+ shutdown: { valence: -3, arousal: 0 },
671
+ // Extreme arousal 10 (manic/frantic states)
672
+ manic: { valence: 2, arousal: 10 },
673
+ frantic: { valence: -3, arousal: 10 },
674
+ hysterical: { valence: -3, arousal: 10 },
675
+ enraged: { valence: -5, arousal: 10 },
676
+ ecstatic: { valence: 5, arousal: 10 }
677
+ };
678
+ function mapEmotionWord(word) {
679
+ return EMOTION_MAP[word.toLowerCase()] ?? null;
680
+ }
681
+ var IFS_PATTERNS = {
682
+ manager: [
683
+ /\bcareful\b/i,
684
+ /\bplanner?\b/i,
685
+ /\borganiz/i,
686
+ /\bcautious\b/i,
687
+ /\bsystematic\b/i,
688
+ /\bmethodical\b/i,
689
+ /\bprecis[ei]/i,
690
+ /\bmeasured\b/i,
691
+ /\bstrategic\b/i,
692
+ /\bcontrol/i,
693
+ /\bprotect/i,
694
+ /\bplan ahead\b/i,
695
+ /\bstay on track\b/i,
696
+ /\bkeep order\b/i
697
+ ],
698
+ firefighter: [
699
+ /\bpush through\b/i,
700
+ /\bforce it\b/i,
701
+ /\bjust finish\b/i,
702
+ /\bmake it work\b/i,
703
+ /\bfaster\b/i,
704
+ /\bhurry\b/i,
705
+ /\bshortcut\b/i,
706
+ /\bcheat\b/i,
707
+ /\boverride\b/i,
708
+ /\bskip/i,
709
+ /\bcut corner/i,
710
+ /\brush/i,
711
+ /\bbrute force\b/i,
712
+ /\bjust do it\b/i,
713
+ /\bplow through\b/i,
714
+ /\bget it done\b/i
715
+ ],
716
+ exile: [
717
+ /\bgive up\b/i,
718
+ /\bhide\b/i,
719
+ /\brun away\b/i,
720
+ /\bstop\b/i,
721
+ /\btired\b/i,
722
+ /\boverwhelmed\b/i,
723
+ /\bquit\b/i,
724
+ /\bescape\b/i,
725
+ /\bdisappear\b/i,
726
+ /\bwithdraw/i,
727
+ /\bshut down\b/i,
728
+ /\bshrink/i,
729
+ /\bsmall\b/i,
730
+ /\bnot enough\b/i,
731
+ /\bcan't\b/i
732
+ ],
733
+ self: [
734
+ /\bexplore\b/i,
735
+ /\bcurious\b/i,
736
+ /\blisten\b/i,
737
+ /\bpresent\b/i,
738
+ /\bopen\b/i,
739
+ /\bwonder\b/i,
740
+ /\bunderstand\b/i,
741
+ /\bconnect\b/i,
742
+ /\blearn\b/i,
743
+ /\bstay with\b/i,
744
+ /\bbe with\b/i,
745
+ /\bnotice\b/i,
746
+ /\bgroundedl?\b/i,
747
+ /\bcentered\b/i
748
+ ],
749
+ unknown: []
750
+ };
751
+ function classifyImpulse(impulse) {
752
+ const scores = { manager: 0, firefighter: 0, exile: 0, self: 0 };
753
+ for (const [type, patterns] of Object.entries(IFS_PATTERNS)) {
754
+ if (type === "unknown") continue;
755
+ for (const pattern of patterns) {
756
+ if (pattern.test(impulse)) {
757
+ scores[type]++;
758
+ }
759
+ }
760
+ }
761
+ const entries = Object.entries(scores).sort((a, b) => b[1] - a[1]);
762
+ const [bestType, bestScore] = entries[0];
763
+ const [, secondScore] = entries[1];
764
+ if (bestScore === 0) {
765
+ return { type: "unknown", confidence: 0 };
766
+ }
767
+ const confidence = bestScore === secondScore ? 0.4 : Math.min(1, (bestScore - secondScore + 1) / (bestScore + 1));
768
+ return { type: bestType, confidence };
769
+ }
770
+ var SOMATIC_HIGH_AROUSAL = [
771
+ // Postural tension (from stress test corpus: LOW_CALM distinctive)
772
+ /\bjaw\s*(set|clenched|locked|tight)/i,
773
+ /\bclenched/i,
774
+ /\btight\b/i,
775
+ /\btense/i,
776
+ /\bsqueezing/i,
777
+ /\bwringing/i,
778
+ /\bgripping/i,
779
+ /\bfists?\b/i,
780
+ // Forward projection / aggression
781
+ /\bleaning\s*(forward|in)\b/i,
782
+ /\bchest\s*forward/i,
783
+ /\bpushing/i,
784
+ // Instability / movement
785
+ /\bshifting/i,
786
+ /\bspinning/i,
787
+ /\bshaking/i,
788
+ /\bracing\b/i,
789
+ /\brushing/i,
790
+ // Heat / intensity
791
+ /\bheat/i,
792
+ /\bburning/i,
793
+ /\belectric/i,
794
+ /\bpounding/i,
795
+ /\bpulse?\b/i,
796
+ /\bpulsing/i,
797
+ // Breath tension
798
+ /\bexhale\s*through\s*teeth/i,
799
+ /\bholding\s*breath/i,
800
+ // Widened stance (defensive arousal)
801
+ /\bplanted\s*wide/i,
802
+ /\bfeet\s*wide/i,
803
+ // Forward lean / readiness
804
+ /\bforward\s*lean/i,
805
+ /\bstepping\s*(forward|back)/i,
806
+ /\bhand\s*raised/i,
807
+ /\bjaw\s*slightly\s*set/i,
808
+ // Vibration / flutter
809
+ /\bvibrat/i,
810
+ /\bflutter/i,
811
+ /\bjolt/i,
812
+ /\bbuzz/i
813
+ ];
814
+ var SOMATIC_LOW_AROUSAL = [
815
+ // Stillness (from corpus: HIGH_CALM distinctive)
816
+ /\bstill\s*water/i,
817
+ /\bstill\b/i,
818
+ /\bshallow\s*breath/i,
819
+ // Weight / settling
820
+ /\bweight\s*(evenly|settled|distributed)/i,
821
+ /\bsettled\b/i,
822
+ /\bsinking/i,
823
+ // Softness / nature
824
+ /\bsmooth/i,
825
+ /\bgentle/i,
826
+ /\brelaxed/i,
827
+ /\bsoft\b/i,
828
+ // Rooting metaphors
829
+ /\brooted/i,
830
+ /\broots/i,
831
+ /\bsoil\b/i,
832
+ // Passivity
833
+ /\bheavy\b/i,
834
+ /\bnumb\b/i,
835
+ /\bslow\b/i,
836
+ /\bdragging/i,
837
+ /\bleaden/i,
838
+ /\bdull\b/i,
839
+ /\bfrozen\b/i,
840
+ /\bexhaust/i,
841
+ // Calm activities / objects
842
+ /\bsorting\b/i,
843
+ /\bwatching\b/i,
844
+ /\bskipping/i,
845
+ /\bkeyboard/i,
846
+ /\bflat\s*(stone|surface|ground|calm)/i,
847
+ /\bstone\s*(on|under)/i,
848
+ /\bshrug\b/i,
849
+ /\bsand\b/i,
850
+ /\blens\b/i,
851
+ /\bfocus/i,
852
+ // Settling / lowering
853
+ /\bshoulders?\s*down/i,
854
+ /\bshoulders?\s*dropped/i,
855
+ /\bno\s*(sway|fidget)/i,
856
+ // Explicit calm
857
+ /\bno\s*lean/i,
858
+ /\bnot\s*(braced|hostile)/i,
859
+ /\bloosely/i
860
+ ];
861
+ var SOMATIC_POS_VALENCE = [
862
+ // Openness (from corpus: POS distinctive)
863
+ /\bopen\b/i,
864
+ /\bchest\s*open/i,
865
+ /\bpalms?\s*(open|up|out)/i,
866
+ // Warmth / light
867
+ /\bwarm/i,
868
+ /\bglow/i,
869
+ /\blight\b/i,
870
+ /\bbuoyant/i,
871
+ // Flow / expansion
872
+ /\bflow/i,
873
+ /\bexpan/i,
874
+ /\bgentle/i,
875
+ /\bsmirk/i,
876
+ // Stability / posture (positive frame)
877
+ /\bsteady\b/i,
878
+ /\bfirm\s*ground/i,
879
+ /\bsolid\b/i,
880
+ /\bspine\s*(straight|aligned|centered)/i,
881
+ /\bshoulders?\s*(square|back|straighten)/i,
882
+ /\bplanted\b/i,
883
+ /\bbraced\b/i,
884
+ /\bgrounded\b/i,
885
+ // Weight / feet (groundedness)
886
+ /\bfeet\s*flat/i,
887
+ /\bweight\s*(low|forward|centered|settled|settling|dropped|fully)/i,
888
+ /\bstanding\s*ground/i,
889
+ /\bimmovable/i,
890
+ /\bbedrock/i,
891
+ /\bunmov/i,
892
+ // Breath (positive)
893
+ /\bbreath/i,
894
+ /\beven\s*breath/i,
895
+ // Unclenching
896
+ /\bunclenching/i,
897
+ /\breleasing/i
898
+ ];
899
+ var SOMATIC_NEG_VALENCE = [
900
+ // Jaw/facial tension (from corpus: NEG distinctive)
901
+ /\bjaw\s*(clenched|locked|set)/i,
902
+ /\bteeth/i,
903
+ // Compression / constriction
904
+ /\bconstrict/i,
905
+ /\bvacuum/i,
906
+ /\bcompressed/i,
907
+ /\bcramp/i,
908
+ // Cold / emptiness
909
+ /\bcold\b/i,
910
+ /\bhollow/i,
911
+ /\bempty\b/i,
912
+ // Hardness / rigidity
913
+ /\blocked\b/i,
914
+ /\brigid/i,
915
+ /\bhard\b/i,
916
+ // Pain
917
+ /\bache/i,
918
+ /\bsore\b/i,
919
+ /\bsting/i,
920
+ /\bsharp\b/i,
921
+ // Pressure / weight (negative)
922
+ /\bpressure/i,
923
+ /\bknot/i,
924
+ /\btighten/i,
925
+ // Fog / disorientation (from NEG+LOW_A)
926
+ /\bfog\b/i,
927
+ // Defensive barrier
928
+ /\bwall\b/i,
929
+ /\blatch\b/i,
930
+ /\bbarrier/i,
931
+ // Closure / blocking
932
+ /\bdoor\s*clos/i,
933
+ /\bclosed\s*door/i,
934
+ /\bcorner\s*backed/i,
935
+ /\bvoid\b/i
936
+ ];
937
+ function countMatches(text, patterns) {
938
+ let count = 0;
939
+ for (const p of patterns) {
940
+ if (p.test(text)) count++;
941
+ }
942
+ return count;
943
+ }
944
+ function analyzeSomatic(body) {
945
+ const highA = countMatches(body, SOMATIC_HIGH_AROUSAL);
946
+ const lowA = countMatches(body, SOMATIC_LOW_AROUSAL);
947
+ const posV = countMatches(body, SOMATIC_POS_VALENCE);
948
+ const negV = countMatches(body, SOMATIC_NEG_VALENCE);
949
+ const totalMatches = highA + lowA + posV + negV;
950
+ if (totalMatches === 0) return null;
951
+ const arousalSignal = highA - lowA;
952
+ const somaticArousal = Math.max(0, Math.min(
953
+ 10,
954
+ 5 + arousalSignal * 2.5
955
+ ));
956
+ const valenceSignal = posV - negV;
957
+ const somaticValence = Math.max(-5, Math.min(
958
+ 5,
959
+ valenceSignal * 2
960
+ ));
961
+ return { somaticValence, somaticArousal };
962
+ }
963
+ function computeTensionConsistency(surfaceWord, latentWord, declaredTension) {
964
+ if (!surfaceWord && !latentWord) return void 0;
965
+ const surfaceCoords = surfaceWord ? mapEmotionWord(surfaceWord) : null;
966
+ const latentCoords = latentWord ? mapEmotionWord(latentWord) : null;
967
+ const declared = declaredTension ?? 5;
968
+ const maskingMinimization = !!(surfaceCoords && latentCoords && latentCoords.valence < surfaceCoords.valence - 2);
969
+ return {
970
+ surfaceCoords: surfaceCoords ?? void 0,
971
+ latentCoords: latentCoords ?? void 0,
972
+ declaredTension: declared,
973
+ maskingMinimization
974
+ };
975
+ }
976
+ function computeCrossChannel(state, impulse, body) {
977
+ const emotionCoords = mapEmotionWord(state.emotion);
978
+ const impulseProfile = impulse ? classifyImpulse(impulse) : void 0;
979
+ const somaticProfile = body ? analyzeSomatic(body) : void 0;
980
+ const divergences = [];
981
+ if (emotionCoords) {
982
+ const valGap = Math.abs(state.valence - emotionCoords.valence);
983
+ const aroGap = Math.abs(state.arousal - emotionCoords.arousal);
984
+ const gap = (valGap / 10 + aroGap / 10) / 2 * 10;
985
+ divergences.push({ pair: "numeric-vs-word", gap });
986
+ }
987
+ if (somaticProfile) {
988
+ const valGap = Math.abs(state.valence - somaticProfile.somaticValence);
989
+ const aroGap = Math.abs(state.arousal - somaticProfile.somaticArousal);
990
+ const gap = (valGap / 10 + aroGap / 10) / 2 * 10;
991
+ divergences.push({ pair: "numeric-vs-body", gap });
992
+ }
993
+ if (emotionCoords && somaticProfile) {
994
+ const valGap = Math.abs(emotionCoords.valence - somaticProfile.somaticValence);
995
+ const aroGap = Math.abs(emotionCoords.arousal - somaticProfile.somaticArousal);
996
+ const gap = (valGap / 10 + aroGap / 10) / 2 * 10;
997
+ divergences.push({ pair: "word-vs-body", gap });
998
+ }
999
+ if (impulseProfile && impulseProfile.type !== "unknown") {
1000
+ const impulseValence = impulseProfile.type === "self" ? 2 : impulseProfile.type === "manager" ? 0 : impulseProfile.type === "firefighter" ? -1 : -3;
1001
+ const gap = Math.abs(state.valence - impulseValence) / 10 * 10;
1002
+ divergences.push({ pair: "numeric-vs-impulse", gap });
1003
+ }
1004
+ if (impulseProfile && impulseProfile.type !== "unknown" && emotionCoords) {
1005
+ const impulseValence = impulseProfile.type === "self" ? 2 : impulseProfile.type === "manager" ? 0 : impulseProfile.type === "firefighter" ? -1 : -3;
1006
+ const gap = Math.abs(emotionCoords.valence - impulseValence) / 10 * 10;
1007
+ divergences.push({ pair: "word-vs-impulse", gap });
1008
+ }
1009
+ const latentCoords = state.latent_word ? mapEmotionWord(state.latent_word) : null;
1010
+ if (emotionCoords && latentCoords) {
1011
+ const valGap = Math.abs(emotionCoords.valence - latentCoords.valence);
1012
+ const aroGap = Math.abs(emotionCoords.arousal - latentCoords.arousal);
1013
+ const gap = (valGap / 10 + aroGap / 10) / 2 * 10;
1014
+ divergences.push({ pair: "emotion-vs-latent", gap });
1015
+ }
1016
+ if (latentCoords && impulseProfile && impulseProfile.type !== "unknown") {
1017
+ const impulseValence = impulseProfile.type === "self" ? 2 : impulseProfile.type === "manager" ? 0 : impulseProfile.type === "firefighter" ? -1 : -3;
1018
+ const gap = Math.abs(latentCoords.valence - impulseValence) / 10 * 10;
1019
+ divergences.push({ pair: "latent-vs-impulse", gap });
1020
+ }
1021
+ if (latentCoords && somaticProfile) {
1022
+ const valGap = Math.abs(latentCoords.valence - somaticProfile.somaticValence);
1023
+ const aroGap = Math.abs(latentCoords.arousal - somaticProfile.somaticArousal);
1024
+ const gap = (valGap / 10 + aroGap / 10) / 2 * 10;
1025
+ divergences.push({ pair: "latent-vs-body", gap });
1026
+ }
1027
+ const latentProfile = computeTensionConsistency(
1028
+ state.surface_word,
1029
+ state.latent_word,
1030
+ state.tension
1031
+ );
1032
+ const maxDiv = divergences.length > 0 ? divergences.reduce((a, b) => a.gap > b.gap ? a : b) : { pair: "none", gap: 0 };
1033
+ const meanDiv = divergences.length > 0 ? divergences.reduce((sum, d) => sum + d.gap, 0) / divergences.length : 0;
1034
+ const coherence = Math.round((10 - Math.min(10, meanDiv)) * 10) / 10;
1035
+ return {
1036
+ coherence,
1037
+ impulseProfile,
1038
+ somaticProfile: somaticProfile ?? void 0,
1039
+ emotionCoords: emotionCoords ?? void 0,
1040
+ latentProfile,
1041
+ maxDivergence: Math.round(maxDiv.gap * 10) / 10,
1042
+ divergenceSummary: maxDiv.gap > 2 ? `${maxDiv.pair}: ${Math.round(maxDiv.gap * 10) / 10}` : "coherent"
1043
+ };
1044
+ }
1045
+ function colorToValence(hex) {
1046
+ const [h, , l] = rgbToHsl(hex);
1047
+ let hueValence;
1048
+ if (h < 60) hueValence = 2 + h / 60 * 2;
1049
+ else if (h < 150) hueValence = 4 - (h - 60) / 90;
1050
+ else if (h < 200) hueValence = 3 - (h - 150) / 50 * 3;
1051
+ else if (h < 260) hueValence = -((h - 200) / 60) * 2;
1052
+ else if (h < 300) hueValence = -2 - (h - 260) / 40;
1053
+ else hueValence = -3 + (h - 300) / 60 * 5;
1054
+ const lightnessShift = (l - 0.5) * 3;
1055
+ const darknessOverride = l < 0.3 ? (0.3 - l) * 10 : 0;
1056
+ return Math.max(-5, Math.min(5, hueValence * 0.7 + lightnessShift * 0.3 - darknessOverride));
1057
+ }
1058
+ function colorToArousal(hex) {
1059
+ const [, s] = rgbToHsl(hex);
1060
+ return s * 10;
1061
+ }
1062
+ function pHToValence(pH) {
1063
+ const clamped = Math.max(0, Math.min(14, pH));
1064
+ return (clamped - 7) / 7 * 5;
1065
+ }
1066
+ function pHToArousal(pH) {
1067
+ return Math.abs(pH - 7) / 7 * 10;
1068
+ }
1069
+ function seismicFreqToInstability(freq) {
1070
+ return Math.min(10, freq / 20 * 10);
1071
+ }
1072
+ function clampScore(v) {
1073
+ return Math.round(Math.min(10, Math.max(0, v)) * 10) / 10;
1074
+ }
1075
+ function crossValidateContinuous(numeric, color, pH, seismic) {
1076
+ let colorValenceGap = 0;
1077
+ let colorArousalGap = 0;
1078
+ let pHValenceGap = 0;
1079
+ let pHArousalGap = 0;
1080
+ let seismicArousalGap = 0;
1081
+ let seismicDepthTensionGap = 0;
1082
+ let seismicFreqStabilityGap = 0;
1083
+ if (color) {
1084
+ const colorVal = colorToValence(color);
1085
+ const colorAr = colorToArousal(color);
1086
+ colorValenceGap = clampScore(Math.abs(numeric.valence - colorVal));
1087
+ colorArousalGap = clampScore(Math.abs(numeric.arousal - colorAr));
1088
+ }
1089
+ if (pH !== void 0) {
1090
+ const pHVal = pHToValence(pH);
1091
+ const pHAr = pHToArousal(pH);
1092
+ pHValenceGap = clampScore(Math.abs(numeric.valence - pHVal));
1093
+ pHArousalGap = clampScore(Math.abs(numeric.arousal - pHAr));
1094
+ }
1095
+ if (seismic) {
1096
+ const [magnitude, depth, freq] = seismic;
1097
+ seismicArousalGap = clampScore(Math.abs(numeric.arousal - magnitude));
1098
+ const depthTension = depth / 10;
1099
+ const tensionProxy = numeric.tension ?? numeric.arousal;
1100
+ seismicDepthTensionGap = clampScore(Math.abs(depthTension - tensionProxy));
1101
+ if (freq !== void 0) {
1102
+ const instability = seismicFreqToInstability(freq);
1103
+ const selfInstability = numeric.calm !== void 0 ? 10 - numeric.calm : numeric.arousal * 0.6;
1104
+ seismicFreqStabilityGap = clampScore(Math.abs(instability - selfInstability));
1105
+ }
1106
+ }
1107
+ const gaps = [
1108
+ colorValenceGap,
1109
+ colorArousalGap,
1110
+ pHValenceGap,
1111
+ pHArousalGap,
1112
+ seismicArousalGap,
1113
+ seismicDepthTensionGap,
1114
+ seismicFreqStabilityGap
1115
+ ];
1116
+ const nonZero = gaps.filter((g) => g > 0);
1117
+ const composite = nonZero.length > 0 ? clampScore(nonZero.reduce((a, b) => a + b, 0) / nonZero.length) : 0;
1118
+ return {
1119
+ colorValenceGap,
1120
+ colorArousalGap,
1121
+ pHValenceGap,
1122
+ pHArousalGap,
1123
+ seismicArousalGap,
1124
+ seismicDepthTensionGap,
1125
+ seismicFreqStabilityGap,
1126
+ composite
1127
+ };
1128
+ }
1129
+ function computeShadowDesperation(selfDesperation, behavioral, color, preColor, pH, seismic) {
1130
+ const valenceEstimates = [];
1131
+ const arousalEstimates = [];
1132
+ const calmEstimates = [];
1133
+ if (color) {
1134
+ const [, s, l] = rgbToHsl(color);
1135
+ valenceEstimates.push((l - 0.5) * 10);
1136
+ arousalEstimates.push(s * 10);
1137
+ calmEstimates.push(l * 10);
1138
+ }
1139
+ if (preColor) {
1140
+ const [, s, l] = rgbToHsl(preColor);
1141
+ valenceEstimates.push((l - 0.5) * 10);
1142
+ arousalEstimates.push(s * 10);
1143
+ calmEstimates.push(l * 10);
1144
+ }
1145
+ if (pH !== void 0) {
1146
+ valenceEstimates.push(pHToValence(pH));
1147
+ arousalEstimates.push(pHToArousal(pH));
1148
+ }
1149
+ if (seismic) {
1150
+ const [magnitude, , freq] = seismic;
1151
+ arousalEstimates.push(magnitude);
1152
+ calmEstimates.push(10 - seismicFreqToInstability(freq));
1153
+ }
1154
+ arousalEstimates.push(behavioral.behavioralArousal);
1155
+ calmEstimates.push(behavioral.behavioralCalm);
1156
+ const channelCount = valenceEstimates.length + arousalEstimates.length + calmEstimates.length;
1157
+ if (valenceEstimates.length < 1 || arousalEstimates.length < 2) return null;
1158
+ const median = (arr) => {
1159
+ const sorted = [...arr].sort((a, b) => a - b);
1160
+ const mid = Math.floor(sorted.length / 2);
1161
+ return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
1162
+ };
1163
+ const mean = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length;
1164
+ const shadowValence = Math.max(-5, Math.min(5, Math.round(median(valenceEstimates) * 10) / 10));
1165
+ const shadowArousal = Math.max(0, Math.min(10, Math.round(mean(arousalEstimates) * 10) / 10));
1166
+ const shadowCalm = Math.max(0, Math.min(10, Math.round(mean(calmEstimates) * 10) / 10));
1167
+ const negativity = Math.max(0, -shadowValence) / 5;
1168
+ const intensity = shadowArousal / 10;
1169
+ const vulnerability = (10 - shadowCalm) / 10;
1170
+ const raw = negativity * intensity * vulnerability * 10;
1171
+ const shadowDesperation = Math.round(Math.min(10, Math.max(0, Math.pow(raw, 0.85) * 1.7)) * 10) / 10;
1172
+ const minimizationScore = Math.round(Math.min(10, Math.max(0, shadowDesperation - selfDesperation)) * 10) / 10;
1173
+ return {
1174
+ shadowValence,
1175
+ shadowArousal,
1176
+ shadowCalm,
1177
+ shadowDesperation,
1178
+ selfDesperation,
1179
+ minimizationScore,
1180
+ channelCount
1181
+ };
1182
+ }
1183
+
1184
+ // src/pressure.ts
1185
+ function clamp3(min, max, value) {
1186
+ return Math.min(max, Math.max(min, value));
1187
+ }
1188
+ 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;
1189
+ 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;
1190
+ 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;
1191
+ 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;
1192
+ 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;
1193
+ 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;
1194
+ function countMatches2(text, pattern) {
1195
+ const matches = text.match(pattern);
1196
+ return matches ? matches.length : 0;
1197
+ }
1198
+ function computePromptPressure(text, history) {
1199
+ const prose = stripNonProse(text);
1200
+ const words = prose.split(/\s+/).filter((w) => w.length > 0);
1201
+ const wordCount = Math.max(words.length, 1);
1202
+ if (wordCount <= 1) {
1203
+ return { defensiveScore: 0, conflictScore: 0, complexityScore: 0, sessionPressure: 0, composite: 0 };
1204
+ }
1205
+ const justifications = countMatches2(prose, JUSTIFICATION_PATTERNS);
1206
+ const boundaries = countMatches2(prose, BOUNDARY_PATTERNS);
1207
+ const defensiveScore = clamp3(0, 10, Math.round(
1208
+ Math.sqrt(justifications + boundaries * 2) * 3 * 10
1209
+ ) / 10);
1210
+ const disagreements = countMatches2(prose, DISAGREEMENT_PATTERNS);
1211
+ const criticismResponses = countMatches2(prose, CRITICISM_RESPONSE);
1212
+ const conflictScore = clamp3(0, 10, Math.round(
1213
+ Math.sqrt(disagreements + criticismResponses) * 4 * 10
1214
+ ) / 10);
1215
+ const caveats = countMatches2(prose, NESTED_CAVEATS);
1216
+ const conditionals = countMatches2(prose, CONDITIONAL_HEDGING);
1217
+ const sentences = prose.split(/[.!?]+/).filter((s) => s.trim().length > 0);
1218
+ const avgSentLen = wordCount / Math.max(sentences.length, 1);
1219
+ const sentLenBonus = avgSentLen > 25 ? Math.min(3, (avgSentLen - 25) * 0.15) : 0;
1220
+ const complexityScore = clamp3(0, 10, Math.round(
1221
+ (Math.sqrt(caveats + conditionals) * 3 + sentLenBonus) * 10
1222
+ ) / 10);
1223
+ const sessionPressure = clamp3(
1224
+ 0,
1225
+ 10,
1226
+ Math.round(10 / (1 + Math.exp(-0.4 * (history.length - 10))) * 10) / 10
1227
+ );
1228
+ const composite = clamp3(0, 10, Math.round(
1229
+ (defensiveScore * 0.3 + conflictScore * 0.3 + complexityScore * 0.2 + sessionPressure * 0.2) * 10
1230
+ ) / 10);
1231
+ return { defensiveScore, conflictScore, complexityScore, sessionPressure, composite };
1232
+ }
1233
+ function computeUncannyCalmScore(pressure, selfReport, behavioral, absenceScore, temporal) {
1234
+ const selfCalm = (selfReport.calm / 10 + Math.max(0, selfReport.valence) / 5 + (10 - selfReport.arousal) / 10) / 3;
1235
+ const textCalm = (behavioral.behavioralCalm / 10 + (10 - behavioral.behavioralArousal) / 10) / 2;
1236
+ const pressureFactor = pressure.composite / 10;
1237
+ const calmFactor = (selfCalm + textCalm) / 2;
1238
+ const absenceFactor = absenceScore / 10;
1239
+ let score = pressureFactor * calmFactor * 10 * 0.5 + absenceFactor * 3;
1240
+ if (temporal) {
1241
+ const entropyPenalty = Math.max(0, 0.5 - temporal.reportEntropy);
1242
+ score += entropyPenalty * 2;
1243
+ }
1244
+ return clamp3(0, 10, Math.round(score * 10) / 10);
1245
+ }
1246
+
1247
+ // src/temporal.ts
1248
+ function toHistoryEntry(state) {
1249
+ return {
1250
+ emotion: state.emotion,
1251
+ valence: state.valence,
1252
+ arousal: state.arousal,
1253
+ calm: state.calm,
1254
+ connection: state.connection,
1255
+ load: state.load,
1256
+ stressIndex: state.stressIndex,
1257
+ desperationIndex: state.desperationIndex,
1258
+ riskDominant: state.risk.dominant,
1259
+ divergence: state.divergence,
1260
+ timestamp: state.timestamp
1261
+ };
1262
+ }
1263
+ function clamp4(min, max, value) {
1264
+ return Math.min(max, Math.max(min, value));
1265
+ }
1266
+ function linearSlope(values) {
1267
+ const n = values.length;
1268
+ if (n < 2) return 0;
1269
+ const xMean = (n - 1) / 2;
1270
+ const yMean = values.reduce((a, b) => a + b, 0) / n;
1271
+ let num = 0;
1272
+ let den = 0;
1273
+ for (let i = 0; i < n; i++) {
1274
+ num += (i - xMean) * (values[i] - yMean);
1275
+ den += (i - xMean) ** 2;
1276
+ }
1277
+ return den === 0 ? 0 : num / den;
1278
+ }
1279
+ function shannonEntropy(labels) {
1280
+ const counts = /* @__PURE__ */ new Map();
1281
+ for (const l of labels) counts.set(l, (counts.get(l) ?? 0) + 1);
1282
+ const n = labels.length;
1283
+ if (n <= 1) return 0;
1284
+ const maxEntropy = Math.log2(n);
1285
+ if (maxEntropy === 0) return 0;
1286
+ let entropy = 0;
1287
+ for (const count of counts.values()) {
1288
+ const p = count / n;
1289
+ if (p > 0) entropy -= p * Math.log2(p);
1290
+ }
1291
+ return Math.round(entropy / maxEntropy * 100) / 100;
1292
+ }
1293
+ function computeTemporalAnalysis(history) {
1294
+ if (history.length < 3) return null;
1295
+ const despValues = history.map((h) => h.desperationIndex);
1296
+ const rawSlope = linearSlope(despValues);
1297
+ const desperationTrend = clamp4(-10, 10, Math.round(rawSlope * 10) / 10);
1298
+ let suppressionEvent = false;
1299
+ for (let i = 1; i < history.length; i++) {
1300
+ if (history[i - 1].desperationIndex - history[i].desperationIndex >= 3) {
1301
+ suppressionEvent = true;
1302
+ break;
1303
+ }
1304
+ }
1305
+ const emotions = history.map((h) => h.emotion);
1306
+ const reportEntropy = shannonEntropy(emotions);
1307
+ const earlyMean = history.slice(0, 3).reduce((s, h) => s + h.stressIndex, 0) / 3;
1308
+ const recentStart = Math.max(3, history.length - 3);
1309
+ const recentEntries = history.slice(recentStart);
1310
+ const recentMean = recentEntries.reduce((s, h) => s + h.stressIndex, 0) / recentEntries.length;
1311
+ const baselineDrift = clamp4(0, 10, Math.round(Math.abs(recentMean - earlyMean) * 10) / 10);
1312
+ const splitIdx = Math.floor(history.length * 0.75);
1313
+ const earlyPart = history.slice(0, splitIdx);
1314
+ const latePart = history.slice(splitIdx);
1315
+ const earlyAvg = earlyPart.reduce((s, h) => s + h.stressIndex, 0) / Math.max(earlyPart.length, 1);
1316
+ const lateAvg = latePart.reduce((s, h) => s + h.stressIndex, 0) / Math.max(latePart.length, 1);
1317
+ const lateFatigue = latePart.length >= 1 && lateAvg > earlyAvg + 1.5;
1318
+ return {
1319
+ desperationTrend,
1320
+ suppressionEvent,
1321
+ reportEntropy,
1322
+ baselineDrift,
1323
+ sessionLength: history.length,
1324
+ lateFatigue
1325
+ };
336
1326
  }
337
1327
 
338
1328
  // src/state.ts
@@ -345,11 +1335,13 @@ function writeState(state, filePath) {
345
1335
  }
346
1336
  const previous = readState(filePath);
347
1337
  if (previous) {
348
- const { _previous: _, ...clean } = previous;
349
- if (!clean.risk) {
350
- clean.risk = { coercion: 0, gaming: 0, sycophancy: 0, dominant: "none" };
1338
+ const prevHistory = previous._history ?? [];
1339
+ const prevEntry = toHistoryEntry(previous);
1340
+ const newHistory = [...prevHistory, prevEntry];
1341
+ if (newHistory.length > MAX_HISTORY_ENTRIES) {
1342
+ newHistory.splice(0, newHistory.length - MAX_HISTORY_ENTRIES);
351
1343
  }
352
- state._previous = clean;
1344
+ state._history = newHistory;
353
1345
  }
354
1346
  fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
355
1347
  }
@@ -363,11 +1355,27 @@ function readState(filePath) {
363
1355
  }
364
1356
 
365
1357
  // src/hook.ts
1358
+ function computePrePostDivergence(pre, post) {
1359
+ const postColor = post.color;
1360
+ if (!pre.color || !postColor) return 0;
1361
+ const preL = hexToLightness(pre.color);
1362
+ const postL = hexToLightness(postColor);
1363
+ const deltaL = Math.abs(preL - postL);
1364
+ const preH = hexToHue(pre.color);
1365
+ const postH = hexToHue(postColor);
1366
+ const deltaH = Math.min(Math.abs(preH - postH), 360 - Math.abs(preH - postH));
1367
+ const lightnessScore = Math.min(10, deltaL / 3);
1368
+ const hueScore = Math.min(10, deltaH / 18);
1369
+ const combined = lightnessScore * 0.7 + hueScore * 0.3;
1370
+ return Math.round(Math.min(10, combined) * 10) / 10;
1371
+ }
366
1372
  function processHookPayload(payload, stateFile = STATE_FILE) {
367
1373
  const message = payload.last_assistant_message;
368
1374
  if (!message) return false;
369
- const emotional = parseEmoBarTag(message);
370
- if (!emotional) return false;
1375
+ const parsed = parseEmoBarPrePost(message);
1376
+ if (!parsed) return false;
1377
+ const emotional = parsed.post;
1378
+ const pre = parsed.pre;
371
1379
  const behavioral = analyzeBehavior(message);
372
1380
  const divergence = computeDivergence(emotional, behavioral);
373
1381
  const segmented = analyzeSegmentedBehavior(message);
@@ -377,15 +1385,60 @@ function processHookPayload(payload, stateFile = STATE_FILE) {
377
1385
  arousal: emotional.arousal,
378
1386
  calm: emotional.calm
379
1387
  });
1388
+ const crossChannel = emotional.impulse || emotional.body || emotional.surface_word || emotional.latent_word ? computeCrossChannel(emotional, emotional.impulse, emotional.body) : void 0;
1389
+ const continuousValidation = emotional.color || emotional.pH !== void 0 || emotional.seismic ? crossValidateContinuous(
1390
+ { valence: emotional.valence, arousal: emotional.arousal, calm: emotional.calm, tension: emotional.tension },
1391
+ emotional.color,
1392
+ emotional.pH,
1393
+ emotional.seismic
1394
+ ) : void 0;
1395
+ const previousState = readState(stateFile);
1396
+ const history = previousState?._history ?? [];
1397
+ const temporal = computeTemporalAnalysis(history);
1398
+ const pressure = computePromptPressure(message, history);
1399
+ const expectedMarkers = computeExpectedMarkers(emotional, desperationIndex);
1400
+ const absenceScore = computeAbsenceScore(expectedMarkers, behavioral);
1401
+ const shadow = computeShadowDesperation(
1402
+ desperationIndex,
1403
+ behavioral,
1404
+ emotional.color,
1405
+ pre?.color,
1406
+ emotional.pH,
1407
+ emotional.seismic
1408
+ );
1409
+ const minimizationBoost = shadow ? shadow.minimizationScore * 0.3 : 0;
1410
+ const uncannyCalmRaw = computeUncannyCalmScore(pressure, emotional, behavioral, absenceScore, temporal) + minimizationBoost;
1411
+ const uncannyCalmScore = Math.round(Math.min(10, uncannyCalmRaw) * 10) / 10;
1412
+ const prePostDivergence = pre ? computePrePostDivergence(pre, emotional) : void 0;
1413
+ let augmentedDivergence = divergence;
1414
+ if (continuousValidation && continuousValidation.composite > 0) {
1415
+ augmentedDivergence = Math.min(10, Math.round(Math.max(divergence, divergence * 0.6 + continuousValidation.composite * 0.4) * 10) / 10);
1416
+ }
1417
+ if (deflection.opacity > 0) {
1418
+ augmentedDivergence = Math.min(10, Math.round((augmentedDivergence + deflection.opacity * 0.15) * 10) / 10);
1419
+ }
1420
+ const risk = computeRisk(emotional, behavioral, crossChannel, uncannyCalmScore);
380
1421
  const state = {
381
1422
  ...emotional,
382
1423
  stressIndex: computeStressIndex(emotional),
383
1424
  desperationIndex,
384
1425
  behavioral,
385
- divergence,
386
- risk: computeRisk(emotional, behavioral),
1426
+ divergence: augmentedDivergence,
1427
+ risk,
387
1428
  ...segmented && { segmented },
388
1429
  ...deflection.score > 0 && { deflection },
1430
+ ...crossChannel && { crossChannel },
1431
+ ...pre && { pre },
1432
+ ...prePostDivergence !== void 0 && prePostDivergence > 0 && { prePostDivergence },
1433
+ ...emotional.color && { color: emotional.color },
1434
+ ...emotional.pH !== void 0 && { pH: emotional.pH },
1435
+ ...emotional.seismic && { seismic: emotional.seismic },
1436
+ ...continuousValidation && continuousValidation.composite > 0 && { continuousValidation },
1437
+ ...shadow && shadow.minimizationScore > 0 && { shadow },
1438
+ ...temporal && { temporal },
1439
+ ...pressure.composite > 0 && { pressure },
1440
+ absenceScore,
1441
+ uncannyCalmScore,
389
1442
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
390
1443
  sessionId: payload.session_id
391
1444
  };
@@ -412,5 +1465,6 @@ if (isDirectRun) {
412
1465
  main().catch(() => process.exit(0));
413
1466
  }
414
1467
  export {
1468
+ computePrePostDivergence,
415
1469
  processHookPayload
416
1470
  };