cyborg-hunter 0.3.0 → 0.4.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 +17 -4
- package/dist/cyborg-hunter.esm.js +28 -35
- package/dist/cyborg-hunter.min.js +3 -3
- package/dist/jspsych-cyborg-hunter.js +1 -1
- package/package.json +1 -1
- package/src/cli/analyzers/triage.js +36 -24
- package/src/cli/config.js +2 -2
- package/src/cli/ingest.js +57 -56
- package/src/cli/renderers/html-index.js +988 -263
- package/src/cli/renderers/trajectories.js +146 -104
- package/src/cli/report.js +15 -1
- package/src/core/monitor.js +47 -30
- package/src/core/signals/dom-protection.js +1 -1
- package/src/core/state-machine.js +1 -1
- package/src/jspsych/extension.js +67 -81
package/src/jspsych/extension.js
CHANGED
|
@@ -1,91 +1,71 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// Two main usage patterns:
|
|
6
|
-
// 1. Monitor everything (default) — every trial gets startTrial/endTrial
|
|
7
|
-
// 2. Explicit opt-in (autoMonitor: false) — monitor only specified trials
|
|
8
|
-
// (trials must pass { trialId: '...' } in their extension parameters)
|
|
9
|
-
//
|
|
10
|
-
// IMPORTANT: This file does NOT import from the core library. It references
|
|
11
|
-
// window.CyborgHunter at runtime. The researcher must load cyborg-hunter.min.js
|
|
12
|
-
// before this script. This avoids bundling the core into the extension.
|
|
1
|
+
// jsPsych extension adapter for cyborg-hunter.
|
|
2
|
+
// Loaded as a script tag AFTER cyborg-hunter.min.js — references
|
|
3
|
+
// window.CyborgHunter at runtime rather than importing the core, so the
|
|
4
|
+
// extension stays a thin (~2KB) adapter even though the core is ~28KB.
|
|
13
5
|
|
|
14
6
|
class CyborgHunterExtension {
|
|
15
|
-
// jsPsych reads this static info object to register the extension.
|
|
16
|
-
// The `data` field declares what on_finish() will return — here, an
|
|
17
|
-
// `integrity` object containing the trial's signal report.
|
|
18
7
|
static info = {
|
|
19
8
|
name: 'cyborg-hunter',
|
|
20
|
-
version
|
|
9
|
+
// Must match the cyborg-hunter library version (src/shared/constants.js
|
|
10
|
+
// and package.json). Hand-bumped on each release; if you forget, the
|
|
11
|
+
// jsPsych developer console shows a stale number — the actual version
|
|
12
|
+
// attached to data is read from window.CyborgHunter.VERSION at runtime.
|
|
13
|
+
version: '0.3.0',
|
|
21
14
|
data: { integrity: { type: 'object' } }
|
|
22
15
|
};
|
|
23
16
|
|
|
24
17
|
constructor(jsPsych) {
|
|
25
18
|
this.jsPsych = jsPsych;
|
|
26
|
-
this.monitor = null;
|
|
27
|
-
this.params = {};
|
|
28
|
-
this._monitoring = false;
|
|
29
|
-
this._trialStart_perfNow = null;
|
|
19
|
+
this.monitor = null;
|
|
20
|
+
this.params = {};
|
|
21
|
+
this._monitoring = false;
|
|
22
|
+
this._trialStart_perfNow = null;
|
|
30
23
|
}
|
|
31
24
|
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
// extensions: [{ type: jsPsychCyborgHunter, params: { preset: 'standard' } }]
|
|
35
|
-
//
|
|
36
|
-
// We pull out extension-specific keys (autoMonitor, excludeTrialTypes) and
|
|
37
|
-
// pass everything else through to CyborgHunter.init() as monitor config.
|
|
25
|
+
// jsPsych calls this once during initJsPsych setup.
|
|
26
|
+
// Splits extension-specific keys out and forwards the rest to the monitor.
|
|
38
27
|
initialize(params) {
|
|
39
28
|
this.params = params;
|
|
40
29
|
const { autoMonitor, excludeTrialTypes, ...monitorConfig } = params;
|
|
41
30
|
|
|
42
|
-
// Look up the global CyborgHunter object (or its backward-compat alias).
|
|
43
|
-
// The researcher must include cyborg-hunter.min.js before this extension.
|
|
44
31
|
const CyborgHunter = window.CyborgHunter || window.IntegrityMonitor;
|
|
45
32
|
if (!CyborgHunter) {
|
|
46
33
|
throw new Error('CyborgHunter not found. Load cyborg-hunter.min.js before the jsPsych extension.');
|
|
47
34
|
}
|
|
48
35
|
|
|
49
|
-
// init() creates the monitor; startSession() begins session-scoped listeners.
|
|
50
36
|
this.monitor = CyborgHunter.init(monitorConfig);
|
|
51
37
|
this.monitor.startSession();
|
|
52
38
|
}
|
|
53
39
|
|
|
54
40
|
// jsPsych 7 unconditionally calls on_start for every trial that lists this
|
|
55
|
-
// extension
|
|
56
|
-
// Without this no-op, the very next trial
|
|
41
|
+
// extension, even though the actual setup happens in on_load (after DOM
|
|
42
|
+
// render). Without this no-op, the very next trial throws
|
|
57
43
|
// "this.extensions['cyborg-hunter'].on_start is not a function".
|
|
58
44
|
on_start(_params) {}
|
|
59
45
|
|
|
60
|
-
// Called
|
|
61
|
-
// `params` here are per-trial extension parameters, e.g.:
|
|
62
|
-
// extensions: [{ type: jsPsychCyborgHunter, params: { trialId: 'rule-3' } }]
|
|
46
|
+
// Called per trial after DOM render. Per-trial extension params arrive here.
|
|
63
47
|
on_load(params) {
|
|
64
48
|
this._monitoring = false;
|
|
65
49
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (!params || !params.trialId) return;
|
|
50
|
+
// Explicit opt-in mode: only monitor trials that pass a trialId.
|
|
51
|
+
if (this.params.autoMonitor === false && !(params && params.trialId)) {
|
|
52
|
+
return;
|
|
70
53
|
}
|
|
71
54
|
|
|
72
|
-
// Auto-monitor with exclusions: skip trial types the
|
|
73
|
-
//
|
|
74
|
-
//
|
|
55
|
+
// Auto-monitor with exclusions: skip trial types named in the config
|
|
56
|
+
// (instructions, fixation, etc.). Trial type name comes from the plugin's
|
|
57
|
+
// static info.name property.
|
|
75
58
|
if (this.params.excludeTrialTypes) {
|
|
76
|
-
const
|
|
77
|
-
const typeName = currentTrial.type?.info?.name || '';
|
|
59
|
+
const typeName = this.jsPsych.getCurrentTrial().type?.info?.name || '';
|
|
78
60
|
if (this.params.excludeTrialTypes.includes(typeName)) return;
|
|
79
61
|
}
|
|
80
62
|
|
|
81
|
-
// Start monitoring this trial. We pass through per-trial params so the
|
|
82
|
-
// core library knows the trial ID, phase, decoy text, and where to scope
|
|
83
|
-
// typing/mouse signals.
|
|
84
63
|
this._monitoring = true;
|
|
85
|
-
//
|
|
86
|
-
// trial-relative tab-away timestamps by direct subtraction
|
|
87
|
-
//
|
|
88
|
-
//
|
|
64
|
+
// Capture performance.now() at trial start so ingest can compute
|
|
65
|
+
// trial-relative tab-away timestamps by direct subtraction. tabAwayEvents
|
|
66
|
+
// store their `start` on the same monotonic clock, so the difference is
|
|
67
|
+
// exact. Without this anchor, the CLI falls back to a heuristic estimator
|
|
68
|
+
// on session-anchored data, which is less precise.
|
|
89
69
|
this._trialStart_perfNow = performance.now();
|
|
90
70
|
const trialIndex = this.jsPsych.getProgress().current_trial_global;
|
|
91
71
|
this.monitor.startTrial({
|
|
@@ -96,23 +76,22 @@ class CyborgHunterExtension {
|
|
|
96
76
|
});
|
|
97
77
|
}
|
|
98
78
|
|
|
99
|
-
// Called when a trial finishes
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
on_finish(
|
|
79
|
+
// Called when a trial finishes. Returns the integrity report, which jsPsych
|
|
80
|
+
// merges into the trial's data under the key declared in static info.data
|
|
81
|
+
// (i.e., data.integrity).
|
|
82
|
+
on_finish(_params) {
|
|
103
83
|
if (!this._monitoring) return {};
|
|
104
84
|
this._monitoring = false;
|
|
105
85
|
const report = this.monitor.endTrial();
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
// to trial-relative ms without heuristic offset estimation.
|
|
86
|
+
// Forward the on_load anchor (see on_load above) so ingest can do exact
|
|
87
|
+
// time arithmetic without re-deriving it.
|
|
109
88
|
report.trialStart_perfNow = this._trialStart_perfNow;
|
|
110
89
|
this._trialStart_perfNow = null;
|
|
111
90
|
return { integrity: report };
|
|
112
91
|
}
|
|
113
92
|
|
|
114
|
-
// Called explicitly by the researcher from
|
|
115
|
-
//
|
|
93
|
+
// Called explicitly by the researcher from initJsPsych({ on_finish }), BEFORE
|
|
94
|
+
// saving data. Example:
|
|
116
95
|
//
|
|
117
96
|
// const jsPsych = initJsPsych({
|
|
118
97
|
// extensions: [{ type: jsPsychCyborgHunter, params: { ... } }],
|
|
@@ -125,42 +104,41 @@ class CyborgHunterExtension {
|
|
|
125
104
|
// Why this isn't automatic: jsPsych 7 has no on_finish_experiment extension
|
|
126
105
|
// hook (only initialize / on_start / on_load / on_finish exist). An earlier
|
|
127
106
|
// version of this wrapper defined on_finish_experiment and silently dropped
|
|
128
|
-
// session data because jsPsych never called it.
|
|
107
|
+
// session data because jsPsych never called it. We surface the boundary
|
|
108
|
+
// explicitly so it can't silently regress.
|
|
129
109
|
//
|
|
130
|
-
//
|
|
131
|
-
// - Scalars (counts, booleans, version)
|
|
132
|
-
//
|
|
133
|
-
// - Arrays
|
|
134
|
-
//
|
|
135
|
-
//
|
|
110
|
+
// Persistence shape (split attachment):
|
|
111
|
+
// - Scalars (counts, booleans, version) → addProperties, so they appear
|
|
112
|
+
// as columns on EVERY trial row. Cheap, queryable from R/pandas.
|
|
113
|
+
// - Arrays + nested objects (sidebarEvents, integrityScore, etc.) →
|
|
114
|
+
// addDataToLastTrial, so they're attached ONCE on the final row.
|
|
115
|
+
// Avoids duplicating kilobytes of identical JSON across every row.
|
|
136
116
|
//
|
|
137
|
-
// After persisting, tears down
|
|
117
|
+
// After persisting, tears down listeners and clears session state.
|
|
138
118
|
finalize() {
|
|
139
119
|
if (!this.monitor) return;
|
|
140
120
|
|
|
141
121
|
try {
|
|
142
|
-
//
|
|
143
|
-
//
|
|
122
|
+
// getSessionReport() is the source of truth: contains both the
|
|
123
|
+
// accumulators and the scoring summary in one shape.
|
|
144
124
|
const report = this.monitor.getSessionReport();
|
|
145
125
|
|
|
146
126
|
const {
|
|
147
|
-
// Arrays —
|
|
127
|
+
// Arrays — attached once on the last trial only (no CSV bloat).
|
|
148
128
|
sidebarEvents = [], keyboardShortcuts = [], windowPositions = [],
|
|
149
129
|
layoutShifts = [], zoomChanges = [], idleGaps = [], extensionInjections = [],
|
|
150
130
|
tabAwaySums = [], charsPerSec = [], aiExtensionsFound = [],
|
|
151
|
-
// Scoring summary — full object
|
|
152
|
-
// to every
|
|
131
|
+
// Scoring summary — full object on last trial; key scalars below
|
|
132
|
+
// also duplicated to every row via addProperties.
|
|
153
133
|
hardScore, softScore, anyHardTriggered, trialsCompleted, softScoreThreshold,
|
|
154
134
|
// Scalars.
|
|
155
135
|
pasteCount = 0, copyCount = 0, dropCount = 0,
|
|
156
136
|
libraryVersion, config,
|
|
157
|
-
//
|
|
158
|
-
//
|
|
137
|
+
// Future-proof: anything new from the monitor flows into integritySession
|
|
138
|
+
// automatically without polluting the per-row scalar columns.
|
|
159
139
|
...extras
|
|
160
140
|
} = report;
|
|
161
141
|
|
|
162
|
-
// Scalars: one column per field, duplicated across every trial row.
|
|
163
|
-
// Cheap, useful for per-trial filtering and quick exclusion queries.
|
|
164
142
|
this.jsPsych.data.addProperties({
|
|
165
143
|
integrityPasteCount: pasteCount,
|
|
166
144
|
integrityCopyCount: copyCount,
|
|
@@ -170,7 +148,6 @@ class CyborgHunterExtension {
|
|
|
170
148
|
cyborgHunterVersion: libraryVersion,
|
|
171
149
|
});
|
|
172
150
|
|
|
173
|
-
// Arrays + nested objects: attached once, on the final trial row.
|
|
174
151
|
this.jsPsych.data.addDataToLastTrial({
|
|
175
152
|
integritySession: {
|
|
176
153
|
pasteCount, copyCount, dropCount,
|
|
@@ -182,17 +159,26 @@ class CyborgHunterExtension {
|
|
|
182
159
|
integrityScore: { hardScore, softScore, anyHardTriggered, trialsCompleted, softScoreThreshold },
|
|
183
160
|
});
|
|
184
161
|
} catch (e) {
|
|
185
|
-
//
|
|
162
|
+
// Don't crash the experiment's end-of-session path — the user's save
|
|
163
|
+
// call (localSave / dispatcher / etc.) MUST still run. But don't fail
|
|
164
|
+
// silently either — drop a marker into the data so analysts can see
|
|
165
|
+
// during ingest that finalize() failed for this participant, plus log
|
|
166
|
+
// to console for local debugging.
|
|
186
167
|
console.warn('[cyborg-hunter] Failed to save session report:', e);
|
|
168
|
+
try {
|
|
169
|
+
this.jsPsych.data.addProperties({
|
|
170
|
+
cyborgHunterFinalizeError: String(e && e.message ? e.message : e)
|
|
171
|
+
});
|
|
172
|
+
} catch (_) { /* if even addProperties fails, the warn above is all we have */ }
|
|
187
173
|
}
|
|
188
174
|
|
|
189
175
|
this.monitor.destroy();
|
|
190
176
|
}
|
|
191
177
|
}
|
|
192
178
|
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
179
|
+
// Module export AND IIFE-friendly window assignment. The build target is IIFE,
|
|
180
|
+
// so `window.jsPsychCyborgHunter` is what researchers reference in their
|
|
181
|
+
// extensions array. The named export exists for completeness.
|
|
196
182
|
export { CyborgHunterExtension };
|
|
197
183
|
if (typeof window !== 'undefined') {
|
|
198
184
|
window.jsPsychCyborgHunter = CyborgHunterExtension;
|