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
package/CHANGELOG.md ADDED
@@ -0,0 +1,110 @@
1
+ # Changelog
2
+
3
+ All notable changes to **cyborg-hunter** are documented here. This project follows
4
+ [Semantic Versioning](https://semver.org).
5
+
6
+ ## [0.7.0] — 2026-07-14
7
+
8
+ ### Added
9
+ - Session replay recorder (`dist/cyborg-hunter-replay.js`, opt-in): captures pointer,
10
+ keys, clipboard, scroll, touch, viewport, and (at the `dom` tier) DOM snapshots +
11
+ mutations. Wire format is jsPsych's `SessionRecording v1` with a `ch_extensions`
12
+ namespace.
13
+ - CLI report gains a per-participant replay viewer (scrub bar, cursor trail, event
14
+ markers); `dom`-tier recordings reconstruct the page in a sandboxed iframe.
15
+ - Autosave and CLI ingest of replay artifacts, with ownership verification and
16
+ reload-collision handling.
17
+ - Guard-honeypot and guard-friction events are captured in the replay stream.
18
+
19
+ ### Changed
20
+ - Replay viewer uses a per-trial camera model for cursor/DOM alignment; legacy
21
+ recordings fall back to a clearly-labeled reduced-alignment mode.
22
+ - Password inputs are redacted unconditionally in replay capture; clipboard events
23
+ record lengths only, not content.
24
+
25
+ ### Fixed
26
+ - `drop` listener was incorrectly gated on the `paste` signal flag.
27
+ - Idle-gap and element-trace timers leaked per session instead of being trial-scoped.
28
+ - Edge-exit analysis silently found nothing on modern payloads due to a mismatched
29
+ time base.
30
+ - Shape-3 (top-level array) ingest dropped outer trial fields and skipped tab-away
31
+ normalization.
32
+ - Malformed replay artifacts no longer abort the report; `autoSave()` no longer
33
+ throws on circular/BigInt data.
34
+
35
+ ## [0.6.2] — 2026-07-12
36
+
37
+ ### Fixed
38
+ - Cut events now count toward the hard-copy screenout (previously recorded but
39
+ never incremented the session copy count).
40
+ - Misconfigured scoring overrides (e.g. a nested typo) now warn instead of
41
+ silently disabling a rule.
42
+ - Fullscreen detection is prefix-aware, fixing false violations on Safari <16.4
43
+ and some iOS WebViews.
44
+ - Honeypot re-initialization no longer inherits a prior run's violations/state.
45
+ - `decoyAnswer: false` per-trial opt-out is now honored (was coerced to `null`).
46
+ - A payload with both `trials` and `responses` no longer loses its integrity trials.
47
+ - A non-array signal field no longer crashes the report; ingest now coerces and warns.
48
+ - A numeric `trialId`/`ruleId` no longer crashes the trajectory renderer.
49
+ - `findGuardViolations()` now scans all trials instead of locking onto the first.
50
+
51
+ ### Added
52
+ - Warnings for an unresolved `participantIdField`, duplicate participant IDs, an
53
+ unmatched `phaseScope` phase name, and a non-numeric `scoring.softScoreThreshold`.
54
+
55
+ ## [0.6.1] — 2026-07-06
56
+
57
+ ### Added
58
+ - `endTrial()` stamps a wall-clock ISO `timestamp` on the trial report.
59
+ - Session-level `tabAwayEvents[]` alongside `tabAwaySums`, preserving full timing
60
+ for tab-aways outside `startTrial`/`endTrial`.
61
+ - `participantIdField` accepts dot-paths (e.g. `"metadata.sessionId"`).
62
+ - `sessionIntegrityPath`, `phaseScope`, and `trajectoryDisplayOrder` config options.
63
+ - `showPlatformId` flag (default off) to render a platform ID as a secondary line
64
+ in the HTML report.
65
+ - `--config-file` accepted as an alias of `--config`.
66
+
67
+ ### Changed
68
+ - `layoutShifts` renamed to `viewportWidthShifts` (old key kept as a deprecated
69
+ alias); the signal measures viewport-width changes, not Web Vitals CLS.
70
+ - Viewport-shift logging is debounced (250ms default) instead of firing per
71
+ animation frame.
72
+ - Trajectory panels are tinted by phase; `triage.md` carries an explicit Tier column.
73
+
74
+ ### Fixed
75
+ - `sessionIntegrityPath` no longer accepts a wrong-shaped object at the resolved
76
+ path, which had silently zeroed downstream signals.
77
+
78
+ ## [0.6.0] — 2026-06-25
79
+
80
+ No public API was removed. Re-running the report on existing data may shift
81
+ triage scores/ordering (sidebar, tab-away, and hard-flag corrections); newly
82
+ collected data no longer saves raw per-keystroke timings by default.
83
+
84
+ ### Changed
85
+ - Sidebar events are counted as distinct openings, not raw log entries.
86
+ - Hard-flag fallback now uses the cumulative session total against the count
87
+ threshold, not per-trial hits.
88
+ - Tab-away binning matches the runtime's strict cutoff.
89
+ - Triage ranks tier-first (hard → soft → clean), then by score.
90
+ - The CLI honors each participant's own saved thresholds instead of generic defaults.
91
+
92
+ ### Added
93
+ - Guard-honeypot self-disclosure surfaced in the report (`honeypot_ai_use`,
94
+ `honeypot_ai_report` columns).
95
+ - `finalize()` persists the runtime config so the CLI can reconstruct each
96
+ participant's screening settings.
97
+
98
+ ### Privacy
99
+ - Raw per-keystroke timings are no longer persisted by default
100
+ (`keystrokeDynamics` toggle); only derived typing speed is kept unless opted in.
101
+
102
+ ### Fixed
103
+ - Restored the authoritative session score for two saved formats that had been
104
+ dropped, causing over-flagging.
105
+ - Session-timeline offset estimation no longer drifts for legacy participants.
106
+
107
+ ## [0.5.1] and earlier
108
+
109
+ See the git tags `v0.4.0`, `v0.5.0`, `v0.5.1` for prior releases (no changelog
110
+ was kept before 0.6.0).
package/CITATION.cff ADDED
@@ -0,0 +1,29 @@
1
+ cff-version: 1.2.0
2
+ title: cyborg-hunter
3
+ message: >-
4
+ If you use this software in academic work, please cite it as below.
5
+ type: software
6
+ authors:
7
+ - family-names: Konuk
8
+ given-names: Can
9
+ - family-names: Btesh
10
+ given-names: Victor
11
+ - family-names: Nunez
12
+ given-names: Jose Luis
13
+ repository-code: https://github.com/konukcan/cyborg-hunter
14
+ abstract: >-
15
+ A JavaScript library and CLI for detecting AI-tool use during browser-based
16
+ behavioral experiments. Captures paste, copy, drag, tab-away, mouse
17
+ trajectories, browser sidebar openings, and other signals that compromise
18
+ data quality on Prolific, MTurk, and classroom studies. Produces a triage
19
+ report ranking participants by suspiciousness.
20
+ keywords:
21
+ - integrity-monitoring
22
+ - cheating-detection
23
+ - online-experiments
24
+ - jspsych
25
+ - prolific
26
+ - behavioral-research
27
+ license: MIT
28
+ version: 0.7.0
29
+ date-released: '2026-07-14'
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Can Konuk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -2,96 +2,134 @@
2
2
 
3
3
  Detects AI-tool use during browser-based behavioral experiments. Captures paste, copy, drag, tab-away, mouse trajectories, browser sidebar openings, and other signals that compromise data quality on Prolific / MTurk / classroom studies. Produces a triage report ranking participants by suspiciousness.
4
4
 
5
- ### Example: 100-participant Prolific pilot
5
+ Optional companion deterrence modules (`extension-guard-friction.js`, `extension-guard-honeypot.js`) ship in the same package: friction enforces fullscreen + blocks sidebars + scrambles content during violations + asks cooperative LLMs to refuse; honeypot exposes both hidden and visible bait fields that AI agents fill while human participants don't see them.
6
6
 
7
- Top of the triage list (excerpt — full ranked CSV in the report):
7
+ ### Example: what a report looks like
8
8
 
9
- | Rank | Participant | Score | Hard | Reason |
10
- |------|-------------|-------|------|--------|
11
- | 1 | 67c1d6d98c8787be36609212 | 4078 | no | 1 tab-away 3–10s; 2 flickers <3s; fast typing on 9 trials; **1015 synthetic insertions** |
12
- | 2 | 69bc21dd22f17b2337957511 | 179 | **YES** | 21 paste events; 3 copy events; 9 tab-aways ≥10s; 3 sidebar events; 21 layout shifts |
13
- | 3 | 69b9f3184e0896ef41b99aa8 | 152 | no | 38 synthetic insertions |
14
- | 4 | 5d350282cec7150015d16494 | 131 | **YES** | 5 paste events; 10 copy events; 22 tab-aways ≥10s; 5 tab-aways 3–10s; 15 flickers <3s |
15
- | 5 | 696a9773a84c5f9d4a930874 | 128 | **YES** | 15 paste events; 23 tab-aways ≥10s; 11 tab-aways 3–10s; 9 flickers <3s |
16
- | 6 | 69b594dccc64df571f092147 | 123 | **YES** | 12 paste events; 16 tab-aways ≥10s; 4 tab-aways 3–10s |
17
- | 7 | 69cd5149cf6fc07126897938 | 120 | **YES** | 15 paste events; 1 copy events; 19 tab-aways ≥10s; 16 flickers <3s |
18
- | 8 | 6314bc49055e150c92f4736e | 112 | **YES** | 12 paste events; 14 tab-aways ≥10s; 46 tab-aways 3–10s; 3 flickers <3s |
9
+ The bundled three-participant synthetic dataset (`examples/synthetic-pilot/` — every number hand-authored, no real participant behind any of it) triages like this:
19
10
 
20
- Of 100 participants, 6 hard-flagged (count thresholds crossed) and 12 soft-flagged (weighted score over threshold). Per-participant tab-away timeline below — this is rank 4, with 42 tab-away events across 15 trials. Reviewers can scan the timeline visually instead of staring at a 30-column CSV:
11
+ | Rank | Participant | Tier | Score | Reason |
12
+ |------|-------------|------|-------|--------|
13
+ | 1 | SYN-HARD-03 | **HARD** | 18 | 2 paste events; 1 copy events; 3 tab-aways ≥10s; 2 flickers ≤3s; fast typing on 1 trials |
14
+ | 2 | SYN-SOFT-02 | soft | 21 | 3 copy events; 1 tab-away ≥10s; 2 tab-aways 3–10s; 1 sidebar event |
15
+ | 3 | SYN-CLEAN-01 | clean | 0 | 1 flicker ≤3s; 1 layout shifts |
21
16
 
22
- ![Tab-away timeline showing 42 tab-away events across 15 trials. Most trials have multiple long red bars (≥10s tab-aways), with some shorter orange (3-10s) and grey flicker (<3s) events.](docs/images/example-tab-timeline.png)
17
+ Ranking is **tier-first** (hard-triggered lead, then soft, then clean), score-descending within a tier — rank 1 outranks rank 2 despite the lower score, because hard evidence beats any accumulation of soft evidence. The score is `5×paste + 5×copy + 3×sidebar + 1×tab-away` (counting tab-aways longer than the participant's tab-away threshold — 3s by default, 5s for the strict preset); synthetic insertions and fast typing are surfaced in the reason but do not drive the score. See [docs/cli-reference.md → Triage scoring](docs/cli-reference.md#triage-scoring). The HTML report for the same dataset:
23
18
 
24
- Two pieces:
19
+ ![HTML report: tier-sorted participant list on the left; per-signal counts, score breakdown, paste evidence, and typing profile for the hard-flagged participant.](docs/assets/report-example.png)
25
20
 
26
- - A small JS library you load in your experiment that records signals as participants work.
27
- - A CLI that reads the resulting data files and renders an HTML report.
21
+ Reproduce the table and page yourself: run `cyborg-hunter report` in `examples/synthetic-pilot/` ([docs/worked-example.md](docs/worked-example.md) interprets every number).
22
+
23
+ ## Repo layout
24
+
25
+ - `src/core/` — signal-collection library (the monitor)
26
+ - `src/jspsych/` — jsPsych extension adapters (one per concern)
27
+ - `src/cli/` + `bin/` — CLI that turns saved data into the triage report
28
+ - `tests/`, `docs/` — tests, package docs
29
+ - `examples/synthetic-pilot/` — synthetic three-participant dataset for trying the CLI (see [docs/worked-example.md](docs/worked-example.md))
28
30
 
29
31
  ## Install
30
32
 
31
- For analysis (CLI):
33
+ CLI (analysis):
32
34
 
33
35
  ```bash
34
36
  npm install -g cyborg-hunter
35
37
  ```
36
38
 
37
- For the experiment page (library), three options:
39
+ Browser (experiment page) — load via unpkg or copy `dist/*.js` into your project:
38
40
 
39
41
  ```html
40
- <!-- Option 1: load via unpkg (always serves latest published version) -->
42
+ <!-- Signal collection -->
41
43
  <script src="https://unpkg.com/cyborg-hunter/dist/cyborg-hunter.min.js"></script>
42
- <script src="https://unpkg.com/cyborg-hunter/dist/jspsych-cyborg-hunter.js"></script>
43
-
44
- <!-- Option 2: pin a specific version (recommended for production studies) -->
45
- <script src="https://unpkg.com/cyborg-hunter@0.3.0/dist/cyborg-hunter.min.js"></script>
46
- <script src="https://unpkg.com/cyborg-hunter@0.3.0/dist/jspsych-cyborg-hunter.js"></script>
47
-
48
- <!-- Option 3: copy dist/cyborg-hunter.min.js + dist/jspsych-cyborg-hunter.js
49
- into your project for fully offline / no-CDN-dependency setups. -->
44
+ <script src="https://unpkg.com/cyborg-hunter/dist/extension-cyborg-hunter.js"></script>
45
+ <!-- Optional: deterrence + bait detection -->
46
+ <script src="https://unpkg.com/cyborg-hunter/dist/extension-guard-friction.js"></script>
47
+ <script src="https://unpkg.com/cyborg-hunter/dist/extension-guard-honeypot.js"></script>
48
+ <!-- Optional: session replay recorder -->
49
+ <script src="https://unpkg.com/cyborg-hunter/dist/cyborg-hunter-replay.js"></script>
50
50
  ```
51
51
 
52
+ For production studies, pin a version: `https://unpkg.com/cyborg-hunter@0.7.0/dist/...`.
53
+
52
54
  ## Plug into a jsPsych experiment
53
55
 
54
56
  ```javascript
55
57
  const jsPsych = initJsPsych({
56
58
  extensions: [
57
- { type: jsPsychCyborgHunter, params: { participantId: subject.id, preset: 'standard' } }
59
+ { type: jsPsychCyborgHunter, params: { participantId: participant_id, preset: 'standard' } },
60
+ { type: jsPsychGuardFriction }, // optional
61
+ { type: jsPsychGuardHoneypot } // optional
58
62
  ],
59
63
  on_finish: function () {
60
- // REQUIRED: jsPsych 7 has no on_finish_experiment extension hook, so the
61
- // session-level signals must be flushed manually before saving data.
62
- jsPsych.extensions['cyborg-hunter'].finalize();
64
+ jsPsych.extensions['guard-friction'].finalize(); // stop friction first
65
+ jsPsych.extensions['guard-honeypot'].finalize(); // then attach data
66
+ jsPsych.extensions['cyborg-hunter'].finalize(); // then save
63
67
  jsPsych.data.get().localSave('csv', 'data.csv');
64
68
  }
65
69
  });
66
70
 
67
- // Opt every trial in to monitoring (single forEach beats editing N trials):
68
- timeline.forEach(t => {
69
- t.extensions = (t.extensions || []).concat([{ type: jsPsychCyborgHunter }]);
70
- });
71
-
71
+ timeline.push(jsPsychGuardFriction.entryTrial()); // user-gesture fullscreen entry
72
+ timeline.forEach(t => t.extensions = (t.extensions || []).concat([
73
+ { type: jsPsychCyborgHunter },
74
+ { type: jsPsychGuardFriction },
75
+ { type: jsPsychGuardHoneypot }
76
+ ]));
72
77
  jsPsych.run(timeline);
73
78
  ```
74
79
 
75
- Full setup walk-through, including standalone (non-jsPsych) usage and per-trial parameters: [`docs/using-cyborg-hunter.md`](docs/using-cyborg-hunter.md).
80
+ `participantId` must exist before `initJsPsych`. Full walk-through (per-trial params, standalone non-jsPsych use, opt-in/exclude modes): [docs/using-cyborg-hunter.md](docs/using-cyborg-hunter.md).
76
81
 
77
82
  ## Generate a report
78
83
 
79
- After data collection, in the directory holding your data files:
80
-
81
84
  ```bash
82
- cyborg-hunter init # writes cyborg-hunter.config.json
85
+ cd <your-data-dir>
86
+ cyborg-hunter init # writes cyborg-hunter.config.json
83
87
  # edit config: dataDir, filePattern, participantIdField
84
- cyborg-hunter report # writes ./cyborg-hunter-report/
88
+ cyborg-hunter report # writes ./cyborg-hunter-report/
85
89
  open cyborg-hunter-report/index.html
86
90
  ```
87
91
 
88
- The report contains:
92
+ Output: `summary.csv` (per-participant columns), `triage.md` (ranked list), `event-log.csv` (chronological events), `images/` (per-participant mouse paths, session timelines, typing profiles), `index.html` (landing page).
93
+
94
+ ## Session replay
95
+
96
+ The optional replay recorder captures what the participant did and (at the
97
+ `dom` tier) what the page looked like, so a flagged session can be reviewed
98
+ visually instead of adjudicated from counts alone. Recordings use jsPsych
99
+ PR #3661's `SessionRecording v1` wire format with a `ch_extensions` block.
100
+
101
+ ```javascript
102
+ // jsPsych: one more extension (declare anywhere; finalize LAST)
103
+ { type: jsPsychCyborgHunterReplay, params: {
104
+ participantId: participant_id,
105
+ tier: 'dom', // 'trace' (default) | 'dom'
106
+ autoSave: { mode: 'datapipe', experimentId: 'ABC123' } } }
107
+ // on_finish: await jsPsych.extensions['cyborg-hunter-replay'].finalize();
108
+ ```
109
+
110
+ ```javascript
111
+ // Standalone (any experiment, no jsPsych)
112
+ const rec = CyborgHunterReplay.attach({ participantId, tier: 'dom',
113
+ autoSave: { mode: 'datapipe', experimentId: 'ABC123' } });
114
+ rec.startSession();
115
+ rec.startTrial({ trialId: 'r1' }); // optional bracketing
116
+ rec.endTrial();
117
+ rec.stopSession('finished');
118
+ await rec.autoSaveNow();
119
+ ```
89
120
 
90
- - `summary.csv` — one row per participant, every signal as a column
91
- - `triage.md` — ranked list with one-line "why flagged" per participant
92
- - `event-log.csv` — chronological copy/paste/drop/tab-away events
93
- - `images/` — per-participant mouse trajectories, tab timelines, typing-speed profiles
94
- - `index.html` — landing page that ties it together
121
+ The artifact saves as `<pid>-replay-<epoch>.json` next to your data; the CLI
122
+ picks it up automatically and the report gains a **Session replay** section
123
+ per participant (lazy-loaded scrub viewer — cursor trail, clicks, away
124
+ bands, and a sandboxed reconstruction of the page for `dom`-tier
125
+ recordings). On `dom`-tier recordings the cursor is verified per
126
+ interaction by a five-way alignment self-check — any click it can't
127
+ confirm draws an explicit uncertain marker instead of a wrong one, and
128
+ recordings made before this guarantee existed replay under a reduced-
129
+ guarantees banner. Password fields are always redacted; see
130
+ [docs/using-cyborg-hunter.md](docs/using-cyborg-hunter.md) for the privacy
131
+ model, data-volume guidance, delivery semantics, and the alignment
132
+ guarantee in full.
95
133
 
96
134
  ## What it detects
97
135
 
@@ -102,56 +140,56 @@ The report contains:
102
140
  | Copy | Clipboard `copy` events | Soft (weighted) |
103
141
  | Tab-away | `visibilitychange` + `blur`/`focus` | Soft (weighted) |
104
142
  | Browser sidebar | `innerWidth` delta + layout compression | Soft (weighted) |
105
- | Suspicious typing speed | chars/sec exceeding the preset's threshold | Soft (weighted) |
106
- | Synthetic insertion | text appearing without keystrokes | Soft (weighted) |
107
- | Foreign input | typing landing outside the experiment container | Soft (weighted) |
108
- | Idle gaps | input inactivity (potential context-switch out) | Soft (weighted) |
109
- | AI-extension content scripts | DOM scan for known extension selectors | Soft (weighted) |
110
- | Mouse trajectories | 20Hz polling + path-efficiency / direction-change metrics | Diagnostic |
143
+ | Suspicious typing speed | chars/sec exceeding preset threshold | Soft (weighted) |
144
+ | Synthetic insertion | text appearing without keystrokes | Diagnostic (collected, not scored) |
145
+ | Foreign input | typing landing outside experiment container | Soft (weighted) |
146
+ | Idle gaps | input inactivity | Diagnostic (collected, not scored) |
147
+ | AI-extension content scripts | DOM scan for known extension selectors | Diagnostic (collected, not scored) |
148
+ | Mouse trajectories | 20Hz polling + path-efficiency metrics | Diagnostic |
111
149
  | Window/screen geometry | polled + resize-event capture, with zoom inference | Diagnostic |
112
150
 
113
- Three presets — `permissive` (pilot), `standard` (default), `strict` (high-stakes). Per-signal thresholds documented in [`docs/signals-reference.md`](docs/signals-reference.md).
151
+ Three presets: `permissive` / `standard` (default) / `strict`. Per-signal thresholds: [docs/signals-reference.md](docs/signals-reference.md).
114
152
 
115
- ## What it doesn't detect
153
+ Tab-aways are captured session-wide, not just during trials: since 0.6.1 the session report keeps a timestamped `tabAwayEvents[]` for every tab-away — including those during consent, tutorial, or study phases — so session timelines can place them. Data saved with older versions keeps only durations (`tabAwaySums`) for off-trial events; the timeline footer counts those as unplaceable. (0.6.1 also renames the `layoutShifts` signal to `viewportWidthShifts` — it measures viewport-width changes, not Web-Vitals CLS — keeping the old key as a deprecated alias.)
116
154
 
117
- - **Native browser AI sidebars.** Chrome's built-in Gemini panel and similar are not browser extensions and leave no extension content scripts. The `innerWidth_delta` heuristic still catches them as a generic sidebar event, but the named-extension column will be empty.
118
- - **Screen-share / second device.** A participant reading questions on screen 1 and querying ChatGPT on a phone produces no detectable trace from the browser.
119
- - **AI text edited and retyped.** A determined participant who copies AI output, retypes it character-by-character, and never tab-switches will look clean. The mouse-trajectory and typing-rhythm signals make this much harder than it sounds, but the tool isn't a polygraph.
155
+ The optional **guard** extensions add: fullscreen / sidebar / focus enforcement (with content-scrambling overlay on violation), AI refusal notices in the DOM, and honeypot fields (hidden + visible bait) that catch sidebar-LLMs and agentic browsers (Browser Use, Operator, Computer Use). The visible bait writes `ai_use` / `ai_report` columns into your saved data, and the report surfaces them in `summary.csv` as `honeypot_ai_use` / `honeypot_ai_report` (plus a "self-reported AI use" note in the triage reason).
120
156
 
121
- These are stated up front so reviewers know what flagging really represents.
157
+ ## What it doesn't detect
122
158
 
123
- ## How it works
159
+ - **Native browser AI sidebars** (Chrome Gemini, Edge Copilot) — they leave no extension content scripts. The `innerWidth_delta` heuristic still catches them as generic sidebar events, but the named-extension column stays empty.
160
+ - **Screen-share / second device** — an LLM on a phone reading the screen leaves no in-browser trace.
161
+ - **AI text edited and retyped** — a determined participant who retypes character-by-character without tab-switching looks clean. Mouse-trajectory and typing-rhythm signals raise the bar but it isn't a polygraph.
124
162
 
125
- The library hooks browser events that are universally available (Clipboard API, Visibility API, MouseEvent, Performance API, MutationObserver) and tracks two scopes in parallel:
163
+ ## Documentation
126
164
 
127
- - **Session-scoped monitors** start at experiment load and run continuously: tab-away, sidebar/extension detection, layout shifts, window position polling.
128
- - **Trial-scoped trackers** bracket each trial: mouse path, typing speed, paste/copy/drop within the trial's response window, idle gaps.
165
+ - [docs/quickstart.md](docs/quickstart.md) — zero to triage report
166
+ - [docs/worked-example.md](docs/worked-example.md) — full pipeline run on the bundled synthetic dataset, outputs interpreted
167
+ - [docs/interpreting-signals.md](docs/interpreting-signals.md) — scores vs tiers, viewport shifts, phase scoping: the common misreadings
168
+ - [docs/release-notes-0.7.0.md](docs/release-notes-0.7.0.md) — what 0.7.0 (and 0.6.2) change in data, config, and reports
169
+ - [docs/release-notes-0.6.1.md](docs/release-notes-0.6.1.md) — the 0.6.1 release notes
170
+ - [docs/using-cyborg-hunter.md](docs/using-cyborg-hunter.md) — full integration guide
171
+ - [docs/signals-reference.md](docs/signals-reference.md) — every signal with thresholds per preset
172
+ - [docs/configuration.md](docs/configuration.md) — config file fields and CLI flags
173
+ - [docs/cli-reference.md](docs/cli-reference.md) — commands and output structure
129
174
 
130
- At the end of each trial, an "integrity" object is attached to that trial's data. At the end of the experiment, session-level rollups are attached to the last trial. The CLI ingests both standard jsPsych CSV output and project-specific JSON formats (`docs/configuration.md` describes the config-file fields).
175
+ ## Development
131
176
 
132
- ## Documentation
133
-
134
- - [`docs/using-cyborg-hunter.md`](docs/using-cyborg-hunter.md) — full integration guide (jsPsych and standalone)
135
- - [`docs/signals-reference.md`](docs/signals-reference.md) — every signal with thresholds per preset
136
- - [`docs/configuration.md`](docs/configuration.md) — config file fields and CLI flags
137
- - [`docs/cli-reference.md`](docs/cli-reference.md) — commands and output structure
138
- - [`docs/design-decisions.md`](docs/design-decisions.md) — why the architecture looks the way it does (two-tier scoring, dual lifecycle, `finalize()`, zoom robustness, etc.)
177
+ Run the test suite with `npm test`. Before publishing, run `scripts/check-public-hygiene.sh` — it fails if any tracked file contains personal or internal-process leakage.
139
178
 
140
179
  ## Citation
141
180
 
142
- If you use cyborg-hunter in academic work, please cite:
143
-
144
181
  ```bibtex
145
- @software{konuk2026cyborghunter,
146
- author = {Konuk, Can},
147
- title = {cyborg-hunter: integrity monitoring for online behavioral experiments},
182
+ @software{konuk_cyborg_hunter,
183
+ author = {Konuk, Can and Btesh, Victor and Nunez, Jose Luis},
184
+ title = {cyborg-hunter: detecting AI-tool use in browser-based behavioral experiments},
148
185
  year = {2026},
186
+ version = {0.7.0},
149
187
  url = {https://github.com/konukcan/cyborg-hunter},
150
- version = {0.3.0}
188
+ license = {MIT}
151
189
  }
152
190
  ```
153
191
 
154
- GitHub also renders a "Cite this repository" button (sidebar of the repo page) from the CITATION.cff file in the root.
192
+ A "Cite this repository" button is rendered from `CITATION.cff`.
155
193
 
156
194
  ## License
157
195
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyborg-hunter",
3
- "version": "0.4.0",
3
+ "version": "0.7.0",
4
4
  "description": "Detect LLM-assisted 'cyborg' behavior in online behavioral experiments",
5
5
  "main": "dist/cyborg-hunter.min.js",
6
6
  "module": "dist/cyborg-hunter.esm.js",
@@ -11,14 +11,20 @@
11
11
  "build": "node build.js",
12
12
  "test:browser": "echo 'Open tests/browser/integrity-monitor.test.html in a browser'",
13
13
  "test:cli": "node --test tests/cli/*.test.js",
14
+ "test:core": "node --test tests/core/*.test.js",
14
15
  "test:jspsych": "node --test tests/jspsych/*.test.js",
15
- "test": "npm run test:cli && npm run test:jspsych"
16
+ "test": "npm run test:core && npm run test:replay && npm run test:cli && npm run test:jspsych",
17
+ "test:replay": "node --test tests/replay/*.test.js",
18
+ "test:browser:replay": "node tests/browser/replay/run-replay-tests.mjs",
19
+ "test:browser:alignment": "node tests/browser/replay/cursor-alignment.battery.mjs"
16
20
  },
17
21
  "files": [
18
22
  "dist/",
19
23
  "bin/",
20
24
  "src/",
21
- "README.md"
25
+ "README.md",
26
+ "CHANGELOG.md",
27
+ "CITATION.cff"
22
28
  ],
23
29
  "keywords": [
24
30
  "integrity",
@@ -47,6 +53,7 @@
47
53
  "canvas": "^3.2.3"
48
54
  },
49
55
  "devDependencies": {
56
+ "ajv": "^8.20.0",
50
57
  "esbuild": "^0.21.0",
51
58
  "happy-dom": "^20.9.0"
52
59
  },
@@ -28,7 +28,10 @@ export function detectEdgeExitForTrial(trial, config) {
28
28
  const events = [];
29
29
 
30
30
  for (const ta of tabAways) {
31
- const taStart = ta.start;
31
+ // Modern ingest normalizes tab-away starts to trial-relative startRel_ms;
32
+ // mouse t is already trial-relative, so both must use the same base.
33
+ // Legacy payloads (no startRel_ms) carried trial-relative start directly.
34
+ const taStart = ta.startRel_ms ?? ta.start;
32
35
  // Find mouse move events in the TIME_WINDOW before this tab-away
33
36
  const beforeTA = mouse.filter(m =>
34
37
  m.type === 'move' && m.t < taStart && m.t > taStart - TIME_WINDOW);
@@ -0,0 +1,83 @@
1
+ // src/cli/analyzers/phase-scope.js
2
+ // Pre-registration phase scoping.
3
+ //
4
+ // Studies that pre-register integrity scoring over a subset of phases (e.g.
5
+ // counting only classification-phase signals, excluding a warm-up or
6
+ // debrief phase) previously had to enforce that in their own analysis
7
+ // pipeline — the CH triage scored the whole session and was misleading read
8
+ // in isolation. config.phaseScope filters which trials feed the ANALYZERS
9
+ // (summary, edge-exit, triage):
10
+ //
11
+ // "phaseScope": { "include": ["classification"] }
12
+ // "phaseScope": { "exclude": ["warmup", "post_task_query", "debrief"] }
13
+ //
14
+ // `include` (when non-empty) keeps only listed phases; `exclude` then removes
15
+ // its phases. A trial without a `phase` field counts as "default" — data whose
16
+ // trials carry no phase labels should scope with `exclude`, not `include`.
17
+ //
18
+ // What scoping does NOT touch:
19
+ // * Renderers still receive the full participant — the evidence (timelines,
20
+ // trajectories, paste texts) stays visible; only the scores are scoped.
21
+ // * Ambient session-level environment signals with no phase attribution
22
+ // (sidebar events, keyboard shortcuts, viewport-width shifts, zoom
23
+ // changes) remain session-wide — they aren't tied to any one phase, so
24
+ // scoping them wouldn't be meaningful. Response-level signals
25
+ // (paste/copy/drop/tab-away/typing) are the ones that scope.
26
+ //
27
+ // Downstream, computeParticipantSummary sees `phaseScoped: true` and switches
28
+ // to per-trial aggregation: session-level tabAwaySums and the authoritative
29
+ // session soft score cover the WHOLE session, so they cannot be used for a
30
+ // scoped verdict.
31
+
32
+ export function applyPhaseScope(participants, phaseScope) {
33
+ const inc = Array.isArray(phaseScope?.include) && phaseScope.include.length > 0
34
+ ? new Set(phaseScope.include) : null;
35
+ const exc = Array.isArray(phaseScope?.exclude) && phaseScope.exclude.length > 0
36
+ ? new Set(phaseScope.exclude) : null;
37
+ // No scope configured → hand back the SAME array (byte-identical pipeline).
38
+ if (!inc && !exc) return participants;
39
+
40
+ return participants.map(p => ({
41
+ ...p,
42
+ trials: (p.trials || []).filter(t => {
43
+ const phase = (t && t.phase) ?? 'default';
44
+ if (inc && !inc.has(phase)) return false;
45
+ if (exc && exc.has(phase)) return false;
46
+ return true;
47
+ }),
48
+ phaseScoped: true,
49
+ }));
50
+ }
51
+
52
+ // Returns the configured phaseScope phase names (from include and exclude)
53
+ // that match NO trial anywhere in the cohort. A non-empty result almost always
54
+ // means a typo (e.g. include:["clasification"]) — which, for an `include`,
55
+ // silently filters EVERY trial from EVERY participant and reports the whole
56
+ // cohort clean. report.js warns on this so the misconfiguration is visible.
57
+ // Exported for testing.
58
+ export function findUnmatchedPhaseScopePhases(participants, phaseScope) {
59
+ const configured = [
60
+ ...(Array.isArray(phaseScope?.include) ? phaseScope.include : []),
61
+ ...(Array.isArray(phaseScope?.exclude) ? phaseScope.exclude : []),
62
+ ];
63
+ if (configured.length === 0) return [];
64
+ const present = new Set();
65
+ for (const p of (participants || [])) {
66
+ for (const t of (p.trials || [])) {
67
+ present.add((t && t.phase) ?? 'default');
68
+ }
69
+ }
70
+ return configured.filter(name => !present.has(name));
71
+ }
72
+
73
+ // Human-readable one-liner for the console summary.
74
+ export function describePhaseScope(phaseScope) {
75
+ const parts = [];
76
+ if (Array.isArray(phaseScope?.include) && phaseScope.include.length > 0) {
77
+ parts.push(`include=[${phaseScope.include.join(', ')}]`);
78
+ }
79
+ if (Array.isArray(phaseScope?.exclude) && phaseScope.exclude.length > 0) {
80
+ parts.push(`exclude=[${phaseScope.exclude.join(', ')}]`);
81
+ }
82
+ return parts.join(' ');
83
+ }