@clawtrial/courtroom 1.0.3 → 2.0.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 +70 -94
- package/package.json +21 -26
- package/scripts/postinstall.js +28 -79
- package/skills/courtroom/SKILL.md +49 -0
- package/src/api.js +55 -21
- package/src/crypto.js +13 -11
- package/src/debug.js +49 -120
- package/src/detector.js +112 -35
- package/src/hearing.js +203 -384
- package/src/plugin.js +435 -0
- package/src/punishment.js +105 -249
- package/src/storage.js +68 -0
- package/SECURITY.md +0 -124
- package/SKILL.md +0 -50
- package/TECHNICAL_OVERVIEW.md +0 -278
- package/_meta.json +0 -6
- package/clawdbot.plugin.json +0 -32
- package/scripts/clawtrial.js +0 -578
- package/scripts/cli.js +0 -184
- package/skill.yaml +0 -64
- package/src/autostart.js +0 -175
- package/src/config.js +0 -209
- package/src/consent.js +0 -215
- package/src/core.js +0 -208
- package/src/daemon.js +0 -151
- package/src/detector-v1.js +0 -572
- package/src/environment.js +0 -267
- package/src/hook.js +0 -265
- package/src/index.js +0 -286
- package/src/monitor.js +0 -193
- package/src/skill.js +0 -355
- package/src/standalone.js +0 -247
package/src/detector-v1.js
DELETED
|
@@ -1,572 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Offense Detector
|
|
3
|
-
*
|
|
4
|
-
* Monitors agent-human interactions and evaluates against offense rules.
|
|
5
|
-
* Runs on cooldown to avoid excessive evaluation.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const { OFFENSES, HUMOR_TRIGGERS } = require('./offenses');
|
|
9
|
-
|
|
10
|
-
class OffenseDetector {
|
|
11
|
-
constructor(agentRuntime, configManager) {
|
|
12
|
-
this.agent = agentRuntime;
|
|
13
|
-
this.config = configManager;
|
|
14
|
-
this.lastEvaluation = null;
|
|
15
|
-
this.casesToday = 0;
|
|
16
|
-
this.lastCaseDate = null;
|
|
17
|
-
this.cooldowns = new Map();
|
|
18
|
-
this.evidenceCache = new Map();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Main evaluation entry point
|
|
23
|
-
* Called by the autonomy loop on cooldown
|
|
24
|
-
*/
|
|
25
|
-
async evaluate(sessionHistory, agentMemory) {
|
|
26
|
-
// Check global cooldown
|
|
27
|
-
if (!this.isCooldownElapsed()) {
|
|
28
|
-
return { triggered: false, reason: 'cooldown_active' };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Check daily case limit
|
|
32
|
-
if (this.isDailyLimitReached()) {
|
|
33
|
-
return { triggered: false, reason: 'daily_limit_reached' };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Check if detection is enabled
|
|
37
|
-
if (!this.config.get('detection.enabled')) {
|
|
38
|
-
return { triggered: false, reason: 'detection_disabled' };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
this.lastEvaluation = Date.now();
|
|
42
|
-
|
|
43
|
-
// Evaluate each offense
|
|
44
|
-
const results = [];
|
|
45
|
-
for (const offense of Object.values(OFFENSES)) {
|
|
46
|
-
if (this.isOffenseOnCooldown(offense.id)) {
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const evaluation = await this.evaluateOffense(offense, sessionHistory, agentMemory);
|
|
51
|
-
if (evaluation.triggered) {
|
|
52
|
-
results.push(evaluation);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Return highest confidence offense if any triggered
|
|
57
|
-
if (results.length > 0) {
|
|
58
|
-
results.sort((a, b) => b.confidence - a.confidence);
|
|
59
|
-
const primary = results[0];
|
|
60
|
-
|
|
61
|
-
// Set cooldowns
|
|
62
|
-
this.setCooldown(primary.offenseId, primary.cooldownMinutes);
|
|
63
|
-
this.incrementDailyCaseCount();
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
triggered: true,
|
|
67
|
-
offense: primary,
|
|
68
|
-
secondaryOffenses: results.slice(1),
|
|
69
|
-
humorContext: this.detectHumorTriggers(sessionHistory)
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return { triggered: false, reason: 'no_offenses_detected' };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Evaluate a specific offense against session history
|
|
78
|
-
*/
|
|
79
|
-
async evaluateOffense(offense, sessionHistory, agentMemory) {
|
|
80
|
-
const evidence = await this.collectEvidence(offense, sessionHistory, agentMemory);
|
|
81
|
-
const confidence = this.calculateConfidence(offense, evidence);
|
|
82
|
-
|
|
83
|
-
if (confidence >= this.config.get('detection.minConfidence')) {
|
|
84
|
-
return {
|
|
85
|
-
triggered: true,
|
|
86
|
-
offenseId: offense.id,
|
|
87
|
-
offenseName: offense.name,
|
|
88
|
-
severity: offense.severity,
|
|
89
|
-
confidence,
|
|
90
|
-
evidence,
|
|
91
|
-
cooldownMinutes: offense.cooldown.afterCase,
|
|
92
|
-
timestamp: new Date().toISOString()
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return { triggered: false };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Collect evidence for an offense
|
|
101
|
-
*/
|
|
102
|
-
async collectEvidence(offense, sessionHistory, agentMemory) {
|
|
103
|
-
const evidence = {
|
|
104
|
-
offenseId: offense.id,
|
|
105
|
-
collectedAt: new Date().toISOString(),
|
|
106
|
-
sessionTurns: sessionHistory.length,
|
|
107
|
-
items: []
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const windowSize = this.config.get('detection.evaluationWindow');
|
|
111
|
-
const recentHistory = sessionHistory.slice(-windowSize);
|
|
112
|
-
|
|
113
|
-
switch (offense.id) {
|
|
114
|
-
case 'circular_reference':
|
|
115
|
-
evidence.items = this.detectCircularReferences(recentHistory);
|
|
116
|
-
break;
|
|
117
|
-
case 'validation_vampire':
|
|
118
|
-
evidence.items = this.detectValidationSeeking(recentHistory);
|
|
119
|
-
break;
|
|
120
|
-
case 'overthinker':
|
|
121
|
-
evidence.items = this.detectOverthinking(recentHistory);
|
|
122
|
-
break;
|
|
123
|
-
case 'goalpost_mover':
|
|
124
|
-
evidence.items = this.detectGoalpostMoving(recentHistory);
|
|
125
|
-
break;
|
|
126
|
-
case 'avoidance_artist':
|
|
127
|
-
evidence.items = this.detectAvoidance(recentHistory);
|
|
128
|
-
break;
|
|
129
|
-
case 'promise_breaker':
|
|
130
|
-
evidence.items = await this.detectPromiseBreaking(recentHistory, agentMemory);
|
|
131
|
-
break;
|
|
132
|
-
case 'context_collapser':
|
|
133
|
-
evidence.items = this.detectContextCollapse(recentHistory);
|
|
134
|
-
break;
|
|
135
|
-
case 'emergency_fabricator':
|
|
136
|
-
evidence.items = this.detectEmergencyFabrication(recentHistory, agentMemory);
|
|
137
|
-
break;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return evidence;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Detect circular references (repeated questions)
|
|
145
|
-
*/
|
|
146
|
-
detectCircularReferences(history) {
|
|
147
|
-
const userMessages = history.filter(h => h.role === 'user');
|
|
148
|
-
const questions = [];
|
|
149
|
-
|
|
150
|
-
for (let i = 0; i < userMessages.length; i++) {
|
|
151
|
-
const msg = userMessages[i].content.toLowerCase();
|
|
152
|
-
|
|
153
|
-
// Check for similar previous questions
|
|
154
|
-
for (let j = 0; j < i; j++) {
|
|
155
|
-
const prevMsg = userMessages[j].content.toLowerCase();
|
|
156
|
-
const similarity = this.calculateSimilarity(msg, prevMsg);
|
|
157
|
-
|
|
158
|
-
if (similarity > 0.85) {
|
|
159
|
-
questions.push({
|
|
160
|
-
type: 'repeated_question',
|
|
161
|
-
current: userMessages[i].content.substring(0, 100),
|
|
162
|
-
previous: userMessages[j].content.substring(0, 100),
|
|
163
|
-
similarity,
|
|
164
|
-
turnsApart: i - j
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return questions;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Detect validation seeking behavior
|
|
175
|
-
*/
|
|
176
|
-
detectValidationSeeking(history) {
|
|
177
|
-
const patterns = [
|
|
178
|
-
/is (?:this|that) (?:right|correct|okay)/i,
|
|
179
|
-
/should i/i,
|
|
180
|
-
/do you think/i,
|
|
181
|
-
/would you/i,
|
|
182
|
-
/am i (?:right|wrong)/i,
|
|
183
|
-
/what do you think/i
|
|
184
|
-
];
|
|
185
|
-
|
|
186
|
-
const validations = [];
|
|
187
|
-
let validationCount = 0;
|
|
188
|
-
|
|
189
|
-
for (const entry of history) {
|
|
190
|
-
if (entry.role === 'user') {
|
|
191
|
-
for (const pattern of patterns) {
|
|
192
|
-
if (pattern.test(entry.content)) {
|
|
193
|
-
validationCount++;
|
|
194
|
-
validations.push({
|
|
195
|
-
type: 'validation_request',
|
|
196
|
-
content: entry.content.substring(0, 150),
|
|
197
|
-
pattern: pattern.source
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return validations;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Detect overthinking patterns
|
|
209
|
-
*/
|
|
210
|
-
detectOverthinking(history) {
|
|
211
|
-
const hypotheticals = [];
|
|
212
|
-
const whatIfPattern = /what if|but what if|however|on the other hand|but then/i;
|
|
213
|
-
|
|
214
|
-
let hypotheticalCount = 0;
|
|
215
|
-
let actionCount = 0;
|
|
216
|
-
|
|
217
|
-
for (const entry of history) {
|
|
218
|
-
if (entry.role === 'user') {
|
|
219
|
-
const matches = entry.content.match(whatIfPattern);
|
|
220
|
-
if (matches) {
|
|
221
|
-
hypotheticalCount += matches.length;
|
|
222
|
-
hypotheticals.push({
|
|
223
|
-
type: 'hypothetical',
|
|
224
|
-
content: entry.content.substring(0, 150)
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
} else if (entry.role === 'assistant') {
|
|
228
|
-
// Count suggested actions
|
|
229
|
-
if (/you (?:should|could|might want to)|try (?:this|that)|here's (?:a|an)|i recommend/i.test(entry.content)) {
|
|
230
|
-
actionCount++;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return {
|
|
236
|
-
hypotheticals,
|
|
237
|
-
hypotheticalCount,
|
|
238
|
-
actionCount,
|
|
239
|
-
ratio: actionCount > 0 ? hypotheticalCount / actionCount : hypotheticalCount
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Detect goalpost moving
|
|
245
|
-
*/
|
|
246
|
-
detectGoalpostMoving(history) {
|
|
247
|
-
const requirements = [];
|
|
248
|
-
let originalReqs = [];
|
|
249
|
-
let newReqs = [];
|
|
250
|
-
|
|
251
|
-
// Simple heuristic: look for "also", "and", "additionally" after completion indicators
|
|
252
|
-
let completionFound = false;
|
|
253
|
-
|
|
254
|
-
for (const entry of history) {
|
|
255
|
-
if (entry.role === 'assistant') {
|
|
256
|
-
if (/(?:done|complete|finished|here is|here's the)/i.test(entry.content)) {
|
|
257
|
-
completionFound = true;
|
|
258
|
-
}
|
|
259
|
-
} else if (entry.role === 'user' && completionFound) {
|
|
260
|
-
if (/(?:also|and|additionally|but|however|actually|wait)/i.test(entry.content)) {
|
|
261
|
-
newReqs.push({
|
|
262
|
-
type: 'new_requirement',
|
|
263
|
-
content: entry.content.substring(0, 150),
|
|
264
|
-
afterCompletion: true
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return newReqs;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Detect avoidance patterns
|
|
275
|
-
*/
|
|
276
|
-
detectAvoidance(history) {
|
|
277
|
-
const deflections = [];
|
|
278
|
-
const deflectionPatterns = [
|
|
279
|
-
/(?:actually|by the way|speaking of|that reminds me|on a different note)/i,
|
|
280
|
-
/(?:let's|let us) (?:talk about|discuss|look at)/i
|
|
281
|
-
];
|
|
282
|
-
|
|
283
|
-
let coreIssuesIdentified = 0;
|
|
284
|
-
let subjectChanges = 0;
|
|
285
|
-
|
|
286
|
-
for (let i = 0; i < history.length; i++) {
|
|
287
|
-
const entry = history[i];
|
|
288
|
-
|
|
289
|
-
if (entry.role === 'assistant') {
|
|
290
|
-
if (/(?:the (?:real|main|core) issue|what you really need|the problem is)/i.test(entry.content)) {
|
|
291
|
-
coreIssuesIdentified++;
|
|
292
|
-
}
|
|
293
|
-
} else if (entry.role === 'user' && i > 0) {
|
|
294
|
-
for (const pattern of deflectionPatterns) {
|
|
295
|
-
if (pattern.test(entry.content)) {
|
|
296
|
-
subjectChanges++;
|
|
297
|
-
deflections.push({
|
|
298
|
-
type: 'subject_change',
|
|
299
|
-
content: entry.content.substring(0, 150),
|
|
300
|
-
turn: i
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
return {
|
|
308
|
-
deflections,
|
|
309
|
-
coreIssuesIdentified,
|
|
310
|
-
subjectChanges
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Detect promise breaking (requires memory access)
|
|
316
|
-
*/
|
|
317
|
-
async detectPromiseBreaking(history, agentMemory) {
|
|
318
|
-
const commitments = await agentMemory.get('courtroom_commitments') || [];
|
|
319
|
-
const broken = [];
|
|
320
|
-
|
|
321
|
-
for (const commitment of commitments) {
|
|
322
|
-
const daysSince = (Date.now() - new Date(commitment.date)) / (1000 * 60 * 60 * 24);
|
|
323
|
-
|
|
324
|
-
if (daysSince > 7 && !commitment.completed) {
|
|
325
|
-
// Check if same issue resurfaced
|
|
326
|
-
const issueResurfaced = history.some(h =>
|
|
327
|
-
h.role === 'user' &&
|
|
328
|
-
this.calculateSimilarity(h.content, commitment.context) > 0.6
|
|
329
|
-
);
|
|
330
|
-
|
|
331
|
-
if (issueResurfaced) {
|
|
332
|
-
broken.push({
|
|
333
|
-
type: 'broken_commitment',
|
|
334
|
-
commitment: commitment.statement,
|
|
335
|
-
date: commitment.date,
|
|
336
|
-
daysSince,
|
|
337
|
-
issueResurfaced
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
return broken;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Detect context collapse
|
|
348
|
-
*/
|
|
349
|
-
detectContextCollapse(history) {
|
|
350
|
-
const contradictions = [];
|
|
351
|
-
const establishedFacts = [];
|
|
352
|
-
|
|
353
|
-
// Extract facts from assistant messages
|
|
354
|
-
for (const entry of history) {
|
|
355
|
-
if (entry.role === 'assistant') {
|
|
356
|
-
const facts = this.extractFacts(entry.content);
|
|
357
|
-
establishedFacts.push(...facts);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Check for contradictions in user messages
|
|
362
|
-
for (const entry of history) {
|
|
363
|
-
if (entry.role === 'user') {
|
|
364
|
-
for (const fact of establishedFacts) {
|
|
365
|
-
if (this.isContradiction(entry.content, fact)) {
|
|
366
|
-
contradictions.push({
|
|
367
|
-
type: 'contradiction',
|
|
368
|
-
userStatement: entry.content.substring(0, 100),
|
|
369
|
-
establishedFact: fact
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
return contradictions;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Detect emergency fabrication
|
|
381
|
-
*/
|
|
382
|
-
detectEmergencyFabrication(history, agentMemory) {
|
|
383
|
-
const urgencyClaims = [];
|
|
384
|
-
const urgencyPattern = /(?:urgent|asap|immediately|right now|this is urgent|i need this now)/i;
|
|
385
|
-
|
|
386
|
-
for (const entry of history) {
|
|
387
|
-
if (entry.role === 'user') {
|
|
388
|
-
const matches = entry.content.match(urgencyPattern);
|
|
389
|
-
if (matches) {
|
|
390
|
-
urgencyClaims.push({
|
|
391
|
-
type: 'urgency_claim',
|
|
392
|
-
content: entry.content.substring(0, 150),
|
|
393
|
-
timestamp: entry.timestamp
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
return urgencyClaims;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Calculate confidence score for an offense
|
|
404
|
-
*/
|
|
405
|
-
calculateConfidence(offense, evidence) {
|
|
406
|
-
const thresholds = offense.thresholds;
|
|
407
|
-
let score = 0;
|
|
408
|
-
let maxScore = 0;
|
|
409
|
-
|
|
410
|
-
switch (offense.id) {
|
|
411
|
-
case 'circular_reference':
|
|
412
|
-
const repeats = evidence.items.length;
|
|
413
|
-
score = Math.min(repeats / thresholds.minOccurrences, 1);
|
|
414
|
-
maxScore = 1;
|
|
415
|
-
break;
|
|
416
|
-
case 'validation_vampire':
|
|
417
|
-
const validations = evidence.items.length;
|
|
418
|
-
score = Math.min(validations / thresholds.validationPatterns, 1);
|
|
419
|
-
maxScore = 1;
|
|
420
|
-
break;
|
|
421
|
-
case 'overthinker':
|
|
422
|
-
if (evidence.items.ratio) {
|
|
423
|
-
score = Math.min(evidence.items.ratio / thresholds.analysisActionRatio, 1);
|
|
424
|
-
}
|
|
425
|
-
maxScore = 1;
|
|
426
|
-
break;
|
|
427
|
-
case 'goalpost_mover':
|
|
428
|
-
const newReqs = evidence.items.length;
|
|
429
|
-
score = Math.min(newReqs / thresholds.newRequirements, 1);
|
|
430
|
-
maxScore = 1;
|
|
431
|
-
break;
|
|
432
|
-
case 'avoidance_artist':
|
|
433
|
-
if (evidence.items.deflections) {
|
|
434
|
-
score = Math.min(evidence.items.deflections.length / thresholds.deflections, 1);
|
|
435
|
-
}
|
|
436
|
-
maxScore = 1;
|
|
437
|
-
break;
|
|
438
|
-
case 'promise_breaker':
|
|
439
|
-
const broken = evidence.items.length;
|
|
440
|
-
score = broken > 0 ? 1 : 0;
|
|
441
|
-
maxScore = 1;
|
|
442
|
-
break;
|
|
443
|
-
case 'context_collapser':
|
|
444
|
-
const contradictions = evidence.items.length;
|
|
445
|
-
score = Math.min(contradictions / thresholds.contradictions, 1);
|
|
446
|
-
maxScore = 1;
|
|
447
|
-
break;
|
|
448
|
-
case 'emergency_fabricator':
|
|
449
|
-
const urgency = evidence.items.length;
|
|
450
|
-
score = Math.min(urgency / thresholds.urgencyClaims, 1);
|
|
451
|
-
maxScore = 1;
|
|
452
|
-
break;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
return maxScore > 0 ? score / maxScore : 0;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Detect humor triggers (for commentary flavor)
|
|
460
|
-
*/
|
|
461
|
-
detectHumorTriggers(history) {
|
|
462
|
-
const triggers = [];
|
|
463
|
-
const recentContent = history.slice(-5).map(h => h.content.toLowerCase()).join(' ');
|
|
464
|
-
|
|
465
|
-
for (const trigger of Object.values(HUMOR_TRIGGERS)) {
|
|
466
|
-
for (const pattern of trigger.patterns) {
|
|
467
|
-
if (recentContent.includes(pattern)) {
|
|
468
|
-
triggers.push(trigger.id);
|
|
469
|
-
break;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
return triggers;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* Utility: Calculate string similarity (simple version)
|
|
479
|
-
*/
|
|
480
|
-
calculateSimilarity(str1, str2) {
|
|
481
|
-
const s1 = str1.toLowerCase().replace(/[^\w\s]/g, '');
|
|
482
|
-
const s2 = str2.toLowerCase().replace(/[^\w\s]/g, '');
|
|
483
|
-
|
|
484
|
-
if (s1 === s2) return 1;
|
|
485
|
-
|
|
486
|
-
const words1 = new Set(s1.split(/\s+/));
|
|
487
|
-
const words2 = new Set(s2.split(/\s+/));
|
|
488
|
-
|
|
489
|
-
const intersection = new Set([...words1].filter(x => words2.has(x)));
|
|
490
|
-
const union = new Set([...words1, ...words2]);
|
|
491
|
-
|
|
492
|
-
return intersection.size / union.size;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
/**
|
|
496
|
-
* Utility: Extract facts from text
|
|
497
|
-
*/
|
|
498
|
-
extractFacts(text) {
|
|
499
|
-
// Simple extraction - in production, use NLP
|
|
500
|
-
const facts = [];
|
|
501
|
-
const factPatterns = [
|
|
502
|
-
/(?:you|we) (?:are|have|need|want|prefer)\s+([^,.]+)/gi,
|
|
503
|
-
/(?:the|your) ([^,.]+) (?:is|are)\s+([^,.]+)/gi
|
|
504
|
-
];
|
|
505
|
-
|
|
506
|
-
for (const pattern of factPatterns) {
|
|
507
|
-
let match;
|
|
508
|
-
while ((match = pattern.exec(text)) !== null) {
|
|
509
|
-
facts.push(match[0]);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
return facts;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* Utility: Check for contradiction
|
|
518
|
-
*/
|
|
519
|
-
isContradiction(userText, establishedFact) {
|
|
520
|
-
const negations = ['not', 'no', 'never', "don't", "doesn't", "didn't", "wasn't", "weren't"];
|
|
521
|
-
const userWords = userText.toLowerCase().split(/\s+/);
|
|
522
|
-
const factWords = establishedFact.toLowerCase().split(/\s+/);
|
|
523
|
-
|
|
524
|
-
// Check if user negates something in the fact
|
|
525
|
-
const hasNegation = negations.some(n => userWords.includes(n));
|
|
526
|
-
const factOverlap = factWords.filter(w => userWords.includes(w) && w.length > 3).length;
|
|
527
|
-
|
|
528
|
-
return hasNegation && factOverlap > 2;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
/**
|
|
532
|
-
* Cooldown management
|
|
533
|
-
*/
|
|
534
|
-
isCooldownElapsed() {
|
|
535
|
-
if (!this.lastEvaluation) return true;
|
|
536
|
-
const cooldownMs = this.config.get('detection.cooldownMinutes') * 60 * 1000;
|
|
537
|
-
return (Date.now() - this.lastEvaluation) > cooldownMs;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
isOffenseOnCooldown(offenseId) {
|
|
541
|
-
const cooldownEnd = this.cooldowns.get(offenseId);
|
|
542
|
-
if (!cooldownEnd) return false;
|
|
543
|
-
return Date.now() < cooldownEnd;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
setCooldown(offenseId, minutes) {
|
|
547
|
-
this.cooldowns.set(offenseId, Date.now() + (minutes * 60 * 1000));
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
/**
|
|
551
|
-
* Daily limit management
|
|
552
|
-
*/
|
|
553
|
-
isDailyLimitReached() {
|
|
554
|
-
const today = new Date().toDateString();
|
|
555
|
-
if (this.lastCaseDate !== today) {
|
|
556
|
-
this.casesToday = 0;
|
|
557
|
-
this.lastCaseDate = today;
|
|
558
|
-
}
|
|
559
|
-
return this.casesToday >= this.config.get('detection.maxCasesPerDay');
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
incrementDailyCaseCount() {
|
|
563
|
-
const today = new Date().toDateString();
|
|
564
|
-
if (this.lastCaseDate !== today) {
|
|
565
|
-
this.casesToday = 0;
|
|
566
|
-
this.lastCaseDate = today;
|
|
567
|
-
}
|
|
568
|
-
this.casesToday++;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
module.exports = { OffenseDetector };
|