cyborg-hunter 0.4.0 → 0.7.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/CITATION.cff +29 -0
  3. package/LICENSE +21 -0
  4. package/README.md +117 -79
  5. package/package.json +10 -3
  6. package/src/cli/analyzers/edge-exit.js +4 -1
  7. package/src/cli/analyzers/phase-scope.js +83 -0
  8. package/src/cli/analyzers/summary.js +161 -28
  9. package/src/cli/analyzers/triage.js +59 -27
  10. package/src/cli/config.js +26 -1
  11. package/src/cli/ingest.js +623 -41
  12. package/src/cli/init.js +1 -1
  13. package/src/cli/renderers/event-log.js +18 -19
  14. package/src/cli/renderers/extensions.js +12 -3
  15. package/src/cli/renderers/html-index.js +163 -24
  16. package/src/cli/renderers/replay-assets.js +177 -0
  17. package/src/cli/renderers/replay-viewer.client.js +1022 -0
  18. package/src/cli/renderers/session-timeline.js +917 -0
  19. package/src/cli/renderers/summary-csv.js +5 -0
  20. package/src/cli/renderers/trajectories.js +69 -8
  21. package/src/cli/renderers/triage-md.js +10 -4
  22. package/src/cli/renderers/typing-profile.js +7 -1
  23. package/src/cli/report.js +42 -8
  24. package/src/core/monitor.js +60 -7
  25. package/src/core/scoring.js +11 -2
  26. package/src/core/signals/browser.js +51 -18
  27. package/src/core/signals/clipboard.js +10 -2
  28. package/src/core/signals/dom-protection.js +9 -0
  29. package/src/core/signals/focus.js +16 -2
  30. package/src/jspsych/extension-cyborg-hunter-replay.js +135 -0
  31. package/src/jspsych/{extension.js → extension-cyborg-hunter.js} +9 -2
  32. package/src/jspsych/extension-guard-friction.js +1164 -0
  33. package/src/jspsych/extension-guard-honeypot.js +492 -0
  34. package/src/replay/capture-dom.js +575 -0
  35. package/src/replay/capture-trace.js +468 -0
  36. package/src/replay/index.js +104 -0
  37. package/src/replay/persistence.js +141 -0
  38. package/src/replay/recorder.js +315 -0
  39. package/src/replay/serializer.js +119 -0
  40. package/src/shared/constants.js +12 -6
  41. package/src/shared/schema.js +5 -0
  42. package/src/shared/validation.js +55 -0
  43. package/dist/cyborg-hunter.esm.js +0 -1527
  44. package/dist/cyborg-hunter.min.js +0 -6
  45. package/dist/jspsych-cyborg-hunter.js +0 -1
  46. package/src/cli/renderers/tab-timeline.js +0 -149
@@ -0,0 +1,135 @@
1
+ // jsPsych extension adapter for the cyborg-hunter session replay recorder.
2
+ // Bundled SELF-CONTAINED (imports the replay core), following the guard-
3
+ // extension precedent: one dist file provides both window.CyborgHunterReplay
4
+ // (standalone use) and window.jsPsychCyborgHunterReplay (jsPsych use).
5
+ //
6
+ // Wiring (order matters at finalize; declaration order is free):
7
+ //
8
+ // const jsPsych = initJsPsych({
9
+ // extensions: [
10
+ // { type: jsPsychCyborgHunter, params: { participantId, preset } },
11
+ // { type: jsPsychGuardFriction },
12
+ // { type: jsPsychGuardHoneypot },
13
+ // { type: jsPsychCyborgHunterReplay, params: {
14
+ // participantId, tier: 'dom',
15
+ // autoSave: { mode: 'datapipe', experimentId: 'ABC123' } } }
16
+ // ],
17
+ // on_finish: async function () {
18
+ // jsPsych.extensions['guard-friction'].finalize();
19
+ // jsPsych.extensions['guard-honeypot'].finalize();
20
+ // jsPsych.extensions['cyborg-hunter'].finalize();
21
+ // await jsPsych.extensions['cyborg-hunter-replay'].finalize();
22
+ // jsPsych.data.get().localSave('csv', 'data.csv');
23
+ // }
24
+ // });
25
+ //
26
+ // Replay finalizes LAST: it pulls CH's session report through a monitor
27
+ // reference stashed at initialize() — never through window.CyborgHunter,
28
+ // whose singleton slot is nulled by CH's destroy(). getSessionReport()
29
+ // remains callable after destroy() (sessionData survives teardown), so the
30
+ // pull works even though CH finalized first.
31
+
32
+ import * as CHReplay from '../replay/index.js';
33
+ import { buildReplayMeta } from '../replay/persistence.js';
34
+
35
+ class CyborgHunterReplayExtension {
36
+ static info = {
37
+ name: 'cyborg-hunter-replay',
38
+ version: '0.7.0',
39
+ data: {} // per-trial return is {}; session meta goes via addProperties
40
+ };
41
+
42
+ constructor(jsPsych) {
43
+ this.jsPsych = jsPsych;
44
+ this.api = null;
45
+ this._chMonitor = null;
46
+ this._lastRecording = null;
47
+ this._monitoring = false;
48
+ }
49
+
50
+ initialize(params) {
51
+ this.params = params || {};
52
+ this.api = CHReplay.attach(this.params);
53
+ this.api.startSession();
54
+ this._stashChMonitor();
55
+ }
56
+
57
+ // The cyborg-hunter extension initializes before this one when declared
58
+ // earlier in the extensions array; if not, retry the stash at finalize.
59
+ _stashChMonitor() {
60
+ try {
61
+ var ch = this.jsPsych && this.jsPsych.extensions &&
62
+ this.jsPsych.extensions['cyborg-hunter'];
63
+ if (ch && ch.monitor) this._chMonitor = ch.monitor;
64
+ } catch (e) { /* standalone replay is fine */ }
65
+ }
66
+
67
+ // jsPsych 7 calls on_start unconditionally for every trial that lists the
68
+ // extension — must exist even as a no-op (see extension-cyborg-hunter.js).
69
+ on_start(_params) {}
70
+
71
+ on_load(params) {
72
+ if (!this.api) return;
73
+ this._monitoring = true;
74
+ var trialIndex = 0;
75
+ var plugin = 'unknown';
76
+ try { trialIndex = this.jsPsych.getProgress().current_trial_global; } catch (e) {}
77
+ try {
78
+ plugin = (this.jsPsych.getCurrentTrial() || {}).type?.info?.name || 'unknown';
79
+ } catch (e) {}
80
+ this.api.startTrial({
81
+ trialId: (params && params.trialId) || 'trial-' + trialIndex,
82
+ plugin: plugin
83
+ });
84
+ }
85
+
86
+ on_finish(_params) {
87
+ if (this.api && this._monitoring) {
88
+ this._monitoring = false;
89
+ this.api.endTrial();
90
+ }
91
+ return {};
92
+ }
93
+
94
+ // Called manually from the experiment's on_finish, AFTER the other three
95
+ // extensions (see header). Serializes, autosaves, attaches the meta
96
+ // pointer. Never throws — the experiment's save path must always run.
97
+ async finalize() {
98
+ if (!this.api) return;
99
+ try {
100
+ try { this.api.stopSession('finished'); } catch (e) { /* already stopped */ }
101
+ if (!this._chMonitor) this._stashChMonitor();
102
+ var chReport = null;
103
+ try {
104
+ chReport = this._chMonitor ? this._chMonitor.getSessionReport() : null;
105
+ } catch (e) {
106
+ console.warn('[cyborg-hunter-replay] could not pull CH session report:', e);
107
+ }
108
+ var result = await this.api.autoSaveNow({ chSessionReport: chReport });
109
+ this._lastRecording = result.recording;
110
+ this.jsPsych.data.addProperties({ integrityReplayMeta: result.meta });
111
+ } catch (e) {
112
+ console.warn('[cyborg-hunter-replay] finalize failed:', e);
113
+ try {
114
+ this.jsPsych.data.addProperties({
115
+ replayFinalizeError: String(e && e.message ? e.message : e)
116
+ });
117
+ } catch (_) { /* nothing left to do */ }
118
+ }
119
+ try { this.api.destroy(); } catch (e) { /* teardown best-effort */ }
120
+ // Null the handle so a second finalize() (e.g. duplicated on_finish
121
+ // wiring) is a clean no-op instead of re-serializing and re-saving.
122
+ this.api = null;
123
+ }
124
+
125
+ // Debug/test access to the finalized recording (also handy in the console
126
+ // during piloting: jsPsych.extensions['cyborg-hunter-replay'].getLastRecording()).
127
+ getLastRecording() {
128
+ return this._lastRecording;
129
+ }
130
+ }
131
+
132
+ export { CyborgHunterReplayExtension };
133
+ if (typeof window !== 'undefined') {
134
+ window.jsPsychCyborgHunterReplay = CyborgHunterReplayExtension;
135
+ }
@@ -10,7 +10,7 @@ class CyborgHunterExtension {
10
10
  // and package.json). Hand-bumped on each release; if you forget, the
11
11
  // jsPsych developer console shows a stale number — the actual version
12
12
  // attached to data is read from window.CyborgHunter.VERSION at runtime.
13
- version: '0.3.0',
13
+ version: '0.7.0',
14
14
  data: { integrity: { type: 'object' } }
15
15
  };
16
16
 
@@ -71,7 +71,10 @@ class CyborgHunterExtension {
71
71
  this.monitor.startTrial({
72
72
  trialId: (params && params.trialId) || `trial-${trialIndex}`,
73
73
  phase: params?.phase || null,
74
- decoyAnswer: params?.decoyAnswer || null,
74
+ // Nullish coalescing (not ||) so an explicit `decoyAnswer: false` — the
75
+ // per-trial "skip the decoy" opt-out — reaches the core as false. With
76
+ // `|| null` it became null, and the core auto-generated a decoy instead.
77
+ decoyAnswer: params?.decoyAnswer ?? null,
75
78
  experimentContainer: params?.experimentContainer || null
76
79
  });
77
80
  }
@@ -154,6 +157,10 @@ class CyborgHunterExtension {
154
157
  sidebarEvents, keyboardShortcuts, windowPositions,
155
158
  layoutShifts, zoomChanges, idleGaps, extensionInjections,
156
159
  tabAwaySums, charsPerSec, aiExtensionsFound,
160
+ // Persist the effective runtime settings (preset + thresholds the CLI
161
+ // consumes) so the report can interpret this participant with the
162
+ // thresholds the library actually used, not analyst-side defaults.
163
+ config,
157
164
  ...extras
158
165
  },
159
166
  integrityScore: { hardScore, softScore, anyHardTriggered, trialsCompleted, softScoreThreshold },