@surbee/cipher 0.1.0 → 0.2.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 +95 -0
- package/dist/checks/index.d.mts +215 -0
- package/dist/checks/index.d.ts +215 -0
- package/dist/checks/index.js +1157 -0
- package/dist/checks/index.mjs +60 -0
- package/dist/chunk-P2MIOVFQ.mjs +1104 -0
- package/dist/index.d.mts +38 -244
- package/dist/index.d.ts +38 -244
- package/dist/index.js +1716 -35
- package/dist/index.mjs +649 -35
- package/dist/types-C8t_T3bP.d.mts +251 -0
- package/dist/types-C8t_T3bP.d.ts +251 -0
- package/package.json +16 -4
- package/src/checks/behavioral.ts +0 -527
- package/src/checks/content.ts +0 -372
- package/src/checks/device.ts +0 -384
- package/src/checks/index.ts +0 -59
- package/src/checks/timing.ts +0 -256
- package/src/cipher.ts +0 -225
- package/src/index.ts +0 -75
- package/src/tiers.ts +0 -507
- package/src/types.ts +0 -366
- package/test/cipher.test.ts +0 -245
- package/test/fixtures.ts +0 -627
- package/tsconfig.json +0 -20
package/src/checks/content.ts
DELETED
|
@@ -1,372 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Content-based checks
|
|
3
|
-
*
|
|
4
|
-
* Analyzes response content for quality and patterns.
|
|
5
|
-
* All checks in this file are offline (no API required).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { CheckResult, ResponseInput, BehavioralMetrics } from '../types';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Common gibberish patterns
|
|
12
|
-
*/
|
|
13
|
-
const GIBBERISH_PATTERNS = [
|
|
14
|
-
/^[a-z]{1,3}$/i, // Single letters or very short
|
|
15
|
-
/(.)\1{4,}/, // Repeated characters (aaaaa)
|
|
16
|
-
/^[^aeiou]{5,}$/i, // No vowels in 5+ chars
|
|
17
|
-
/^(asdf|qwerty|zxcv|wasd)/i, // Keyboard mashing
|
|
18
|
-
/^[0-9]+$/, // Only numbers
|
|
19
|
-
/^(.+?)\1{2,}$/, // Repeated patterns (abcabcabc)
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Low effort indicators
|
|
24
|
-
*/
|
|
25
|
-
const LOW_EFFORT_RESPONSES = [
|
|
26
|
-
'n/a',
|
|
27
|
-
'na',
|
|
28
|
-
'none',
|
|
29
|
-
'nothing',
|
|
30
|
-
'idk',
|
|
31
|
-
'i dont know',
|
|
32
|
-
"i don't know",
|
|
33
|
-
'no comment',
|
|
34
|
-
'no',
|
|
35
|
-
'yes',
|
|
36
|
-
'ok',
|
|
37
|
-
'okay',
|
|
38
|
-
'good',
|
|
39
|
-
'fine',
|
|
40
|
-
'whatever',
|
|
41
|
-
'asdf',
|
|
42
|
-
'test',
|
|
43
|
-
'.',
|
|
44
|
-
'-',
|
|
45
|
-
'...',
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Check: Minimal Effort
|
|
50
|
-
*
|
|
51
|
-
* Detects very short or low-quality text responses.
|
|
52
|
-
*/
|
|
53
|
-
export function checkMinimalEffort(responses: ResponseInput[]): CheckResult {
|
|
54
|
-
const textResponses = responses.filter(
|
|
55
|
-
r => r.questionType === 'text' || (!r.questionType && r.answer.length > 0)
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
if (textResponses.length === 0) {
|
|
59
|
-
return {
|
|
60
|
-
checkId: 'minimal_effort',
|
|
61
|
-
passed: true,
|
|
62
|
-
score: 0,
|
|
63
|
-
details: 'No text responses to analyze',
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
let lowEffortCount = 0;
|
|
68
|
-
let totalLength = 0;
|
|
69
|
-
|
|
70
|
-
for (const response of textResponses) {
|
|
71
|
-
const answer = response.answer.toLowerCase().trim();
|
|
72
|
-
totalLength += answer.length;
|
|
73
|
-
|
|
74
|
-
// Check for known low-effort responses
|
|
75
|
-
if (LOW_EFFORT_RESPONSES.includes(answer)) {
|
|
76
|
-
lowEffortCount++;
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Check for very short responses (< 10 chars for open text)
|
|
81
|
-
if (answer.length < 10) {
|
|
82
|
-
lowEffortCount++;
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Check for gibberish patterns
|
|
87
|
-
for (const pattern of GIBBERISH_PATTERNS) {
|
|
88
|
-
if (pattern.test(answer)) {
|
|
89
|
-
lowEffortCount++;
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const lowEffortRatio = lowEffortCount / textResponses.length;
|
|
96
|
-
const avgLength = totalLength / textResponses.length;
|
|
97
|
-
|
|
98
|
-
if (lowEffortRatio > 0.7) {
|
|
99
|
-
return {
|
|
100
|
-
checkId: 'minimal_effort',
|
|
101
|
-
passed: false,
|
|
102
|
-
score: 1.0,
|
|
103
|
-
details: 'Most responses are low effort or gibberish',
|
|
104
|
-
data: { lowEffortRatio, avgLength, lowEffortCount },
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (lowEffortRatio > 0.4) {
|
|
109
|
-
return {
|
|
110
|
-
checkId: 'minimal_effort',
|
|
111
|
-
passed: true,
|
|
112
|
-
score: 0.6,
|
|
113
|
-
details: 'Many responses appear low effort',
|
|
114
|
-
data: { lowEffortRatio, avgLength },
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (avgLength < 20) {
|
|
119
|
-
return {
|
|
120
|
-
checkId: 'minimal_effort',
|
|
121
|
-
passed: true,
|
|
122
|
-
score: 0.4,
|
|
123
|
-
details: 'Responses are quite short on average',
|
|
124
|
-
data: { avgLength },
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
checkId: 'minimal_effort',
|
|
130
|
-
passed: true,
|
|
131
|
-
score: 0,
|
|
132
|
-
data: { lowEffortRatio, avgLength },
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Check: Straight-Lining
|
|
138
|
-
*
|
|
139
|
-
* Detects selecting the same option repeatedly in multiple choice questions.
|
|
140
|
-
*/
|
|
141
|
-
export function checkStraightLining(responses: ResponseInput[]): CheckResult {
|
|
142
|
-
const scaleResponses = responses.filter(
|
|
143
|
-
r => r.questionType === 'rating' ||
|
|
144
|
-
r.questionType === 'scale' ||
|
|
145
|
-
r.questionType === 'multiple_choice'
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
if (scaleResponses.length < 4) {
|
|
149
|
-
return {
|
|
150
|
-
checkId: 'straight_line_answers',
|
|
151
|
-
passed: true,
|
|
152
|
-
score: 0,
|
|
153
|
-
details: 'Not enough scale/choice questions',
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Count consecutive identical answers
|
|
158
|
-
let maxConsecutive = 1;
|
|
159
|
-
let currentConsecutive = 1;
|
|
160
|
-
|
|
161
|
-
for (let i = 1; i < scaleResponses.length; i++) {
|
|
162
|
-
if (scaleResponses[i].answer === scaleResponses[i - 1].answer) {
|
|
163
|
-
currentConsecutive++;
|
|
164
|
-
maxConsecutive = Math.max(maxConsecutive, currentConsecutive);
|
|
165
|
-
} else {
|
|
166
|
-
currentConsecutive = 1;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Also check for all same answer
|
|
171
|
-
const answers = scaleResponses.map(r => r.answer);
|
|
172
|
-
const uniqueAnswers = new Set(answers);
|
|
173
|
-
const uniqueRatio = uniqueAnswers.size / scaleResponses.length;
|
|
174
|
-
|
|
175
|
-
if (uniqueRatio < 0.15) {
|
|
176
|
-
return {
|
|
177
|
-
checkId: 'straight_line_answers',
|
|
178
|
-
passed: false,
|
|
179
|
-
score: 1.0,
|
|
180
|
-
details: 'Nearly all answers are identical',
|
|
181
|
-
data: { uniqueRatio, uniqueAnswers: uniqueAnswers.size, total: scaleResponses.length },
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (maxConsecutive >= 5 || uniqueRatio < 0.25) {
|
|
186
|
-
return {
|
|
187
|
-
checkId: 'straight_line_answers',
|
|
188
|
-
passed: false,
|
|
189
|
-
score: 0.8,
|
|
190
|
-
details: 'Strong straight-lining pattern detected',
|
|
191
|
-
data: { maxConsecutive, uniqueRatio },
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (maxConsecutive >= 4) {
|
|
196
|
-
return {
|
|
197
|
-
checkId: 'straight_line_answers',
|
|
198
|
-
passed: true,
|
|
199
|
-
score: 0.4,
|
|
200
|
-
details: 'Some consecutive identical answers',
|
|
201
|
-
data: { maxConsecutive },
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return {
|
|
206
|
-
checkId: 'straight_line_answers',
|
|
207
|
-
passed: true,
|
|
208
|
-
score: 0,
|
|
209
|
-
data: { maxConsecutive, uniqueRatio },
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Check: Excessive Tab Switching
|
|
215
|
-
*
|
|
216
|
-
* Detects frequent tab/window changes which might indicate:
|
|
217
|
-
* - Looking up answers
|
|
218
|
-
* - Using AI to generate responses
|
|
219
|
-
* - Distracted/inattentive respondent
|
|
220
|
-
*/
|
|
221
|
-
export function checkExcessiveTabSwitching(metrics?: BehavioralMetrics): CheckResult {
|
|
222
|
-
if (!metrics) {
|
|
223
|
-
return {
|
|
224
|
-
checkId: 'excessive_tab_switching',
|
|
225
|
-
passed: true,
|
|
226
|
-
score: 0,
|
|
227
|
-
details: 'No behavioral data',
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const { tabSwitchCount, duration } = metrics;
|
|
232
|
-
const durationMinutes = (duration || 1) / 60000;
|
|
233
|
-
|
|
234
|
-
// Calculate switches per minute
|
|
235
|
-
const switchesPerMinute = tabSwitchCount / durationMinutes;
|
|
236
|
-
|
|
237
|
-
if (switchesPerMinute > 5) {
|
|
238
|
-
return {
|
|
239
|
-
checkId: 'excessive_tab_switching',
|
|
240
|
-
passed: false,
|
|
241
|
-
score: 0.8,
|
|
242
|
-
details: 'Excessive tab switching detected',
|
|
243
|
-
data: { switchesPerMinute, tabSwitchCount },
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (switchesPerMinute > 2) {
|
|
248
|
-
return {
|
|
249
|
-
checkId: 'excessive_tab_switching',
|
|
250
|
-
passed: true,
|
|
251
|
-
score: 0.4,
|
|
252
|
-
details: 'Above average tab switching',
|
|
253
|
-
data: { switchesPerMinute },
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return {
|
|
258
|
-
checkId: 'excessive_tab_switching',
|
|
259
|
-
passed: true,
|
|
260
|
-
score: 0,
|
|
261
|
-
data: { switchesPerMinute },
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Check: Window Focus Loss
|
|
267
|
-
*
|
|
268
|
-
* Detects extended periods where the survey was not in focus.
|
|
269
|
-
*/
|
|
270
|
-
export function checkWindowFocusLoss(metrics?: BehavioralMetrics): CheckResult {
|
|
271
|
-
if (!metrics) {
|
|
272
|
-
return {
|
|
273
|
-
checkId: 'window_focus_loss',
|
|
274
|
-
passed: true,
|
|
275
|
-
score: 0,
|
|
276
|
-
details: 'No behavioral data',
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const { totalBlurDuration, duration } = metrics;
|
|
281
|
-
const blurRatio = totalBlurDuration / (duration || 1);
|
|
282
|
-
|
|
283
|
-
if (blurRatio > 0.6) {
|
|
284
|
-
return {
|
|
285
|
-
checkId: 'window_focus_loss',
|
|
286
|
-
passed: false,
|
|
287
|
-
score: 0.8,
|
|
288
|
-
details: 'Majority of time spent away from survey',
|
|
289
|
-
data: { blurRatio, totalBlurDuration },
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (blurRatio > 0.3) {
|
|
294
|
-
return {
|
|
295
|
-
checkId: 'window_focus_loss',
|
|
296
|
-
passed: true,
|
|
297
|
-
score: 0.4,
|
|
298
|
-
details: 'Significant time away from survey',
|
|
299
|
-
data: { blurRatio },
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return {
|
|
304
|
-
checkId: 'window_focus_loss',
|
|
305
|
-
passed: true,
|
|
306
|
-
score: 0,
|
|
307
|
-
data: { blurRatio },
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Check for duplicate/copy-paste patterns across responses
|
|
313
|
-
*/
|
|
314
|
-
export function checkDuplicateResponses(responses: ResponseInput[]): CheckResult {
|
|
315
|
-
const textResponses = responses
|
|
316
|
-
.filter(r => r.answer.length > 20)
|
|
317
|
-
.map(r => r.answer.toLowerCase().trim());
|
|
318
|
-
|
|
319
|
-
if (textResponses.length < 2) {
|
|
320
|
-
return {
|
|
321
|
-
checkId: 'minimal_effort', // Use existing check ID
|
|
322
|
-
passed: true,
|
|
323
|
-
score: 0,
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Check for identical or near-identical responses
|
|
328
|
-
let duplicateCount = 0;
|
|
329
|
-
for (let i = 0; i < textResponses.length; i++) {
|
|
330
|
-
for (let j = i + 1; j < textResponses.length; j++) {
|
|
331
|
-
if (textResponses[i] === textResponses[j]) {
|
|
332
|
-
duplicateCount++;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (duplicateCount > 0) {
|
|
338
|
-
const possiblePairs = (textResponses.length * (textResponses.length - 1)) / 2;
|
|
339
|
-
const duplicateRatio = duplicateCount / possiblePairs;
|
|
340
|
-
|
|
341
|
-
if (duplicateRatio > 0.3) {
|
|
342
|
-
return {
|
|
343
|
-
checkId: 'minimal_effort',
|
|
344
|
-
passed: false,
|
|
345
|
-
score: 0.7,
|
|
346
|
-
details: 'Multiple identical responses detected',
|
|
347
|
-
data: { duplicateCount, duplicateRatio },
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return {
|
|
353
|
-
checkId: 'minimal_effort',
|
|
354
|
-
passed: true,
|
|
355
|
-
score: 0,
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Run all content checks
|
|
361
|
-
*/
|
|
362
|
-
export function runContentChecks(
|
|
363
|
-
responses: ResponseInput[],
|
|
364
|
-
metrics?: BehavioralMetrics
|
|
365
|
-
): CheckResult[] {
|
|
366
|
-
return [
|
|
367
|
-
checkMinimalEffort(responses),
|
|
368
|
-
checkStraightLining(responses),
|
|
369
|
-
checkExcessiveTabSwitching(metrics),
|
|
370
|
-
checkWindowFocusLoss(metrics),
|
|
371
|
-
];
|
|
372
|
-
}
|