cyborg-hunter 0.5.0 → 0.7.2

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 (58) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/CITATION.cff +29 -0
  3. package/LICENSE +21 -0
  4. package/README.md +80 -21
  5. package/bin/cyborg-hunter.js +6 -2
  6. package/dist/cyborg-hunter-replay.js +3 -0
  7. package/dist/cyborg-hunter.esm.js +114 -22
  8. package/dist/cyborg-hunter.min.js +3 -3
  9. package/dist/extension-cyborg-hunter.js +1 -1
  10. package/dist/extension-guard-friction.js +5 -5
  11. package/dist/extension-guard-honeypot.js +1 -1
  12. package/package.json +15 -3
  13. package/src/cli/analyzers/edge-exit.js +4 -1
  14. package/src/cli/analyzers/phase-scope.js +83 -0
  15. package/src/cli/analyzers/summary.js +161 -28
  16. package/src/cli/analyzers/triage.js +59 -27
  17. package/src/cli/config.js +26 -1
  18. package/src/cli/extract-core.js +552 -0
  19. package/src/cli/ingest.js +250 -193
  20. package/src/cli/init.js +1 -1
  21. package/src/cli/preview-entry.js +36 -0
  22. package/src/cli/renderers/event-log.js +18 -19
  23. package/src/cli/renderers/extensions.js +12 -3
  24. package/src/cli/renderers/html-index-core.js +1273 -0
  25. package/src/cli/renderers/html-index.js +13 -1047
  26. package/src/cli/renderers/replay-assets.js +67 -0
  27. package/src/cli/renderers/replay-viewer.client.js +1185 -0
  28. package/src/cli/renderers/session-timeline-core.js +907 -0
  29. package/src/cli/renderers/session-timeline.js +33 -0
  30. package/src/cli/renderers/summary-csv.js +5 -0
  31. package/src/cli/renderers/trajectories-core.js +717 -0
  32. package/src/cli/renderers/trajectories.js +31 -635
  33. package/src/cli/renderers/triage-md.js +10 -4
  34. package/src/cli/renderers/typing-profile-core.js +211 -0
  35. package/src/cli/renderers/typing-profile.js +16 -186
  36. package/src/cli/report.js +42 -8
  37. package/src/core/monitor.js +77 -7
  38. package/src/core/scoring.js +11 -2
  39. package/src/core/signals/browser.js +51 -18
  40. package/src/core/signals/clipboard.js +10 -2
  41. package/src/core/signals/dom-protection.js +9 -0
  42. package/src/core/signals/focus.js +16 -2
  43. package/src/jspsych/extension-cyborg-hunter-replay.js +135 -0
  44. package/src/jspsych/extension-cyborg-hunter.js +9 -2
  45. package/src/jspsych/extension-guard-friction.js +43 -14
  46. package/src/jspsych/extension-guard-honeypot.js +25 -1
  47. package/src/replay/capture-dom.js +575 -0
  48. package/src/replay/capture-trace.js +468 -0
  49. package/src/replay/index.js +104 -0
  50. package/src/replay/persistence.js +141 -0
  51. package/src/replay/recorder.js +315 -0
  52. package/src/replay/serializer.js +119 -0
  53. package/src/replay/viewer-model.js +125 -0
  54. package/src/shared/constants.js +12 -6
  55. package/src/shared/paths.js +20 -0
  56. package/src/shared/schema.js +5 -0
  57. package/src/shared/validation.js +55 -0
  58. package/src/cli/renderers/tab-timeline.js +0 -149
package/CHANGELOG.md ADDED
@@ -0,0 +1,156 @@
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.2] — 2026-07-29
7
+
8
+ ### Added
9
+ - Replay viewer: a keycast overlay at the bottom of the stage shows keys as
10
+ they're pressed (chips appear on keydown, fade after keyup), including a
11
+ redacted chip for keystrokes captured with `keys:'full'` inside a
12
+ redacted field. Recordings made with `keys:'off'` show no keycast, since
13
+ there's no key data to show.
14
+ - Replay viewer: the buffer-cap notice is now an expandable explanation
15
+ (was a one-line chip) covering what `maxEventsPerTrial`/`maxCharsPerTrial`
16
+ actually cap (per trial, not the whole session), what happens when a
17
+ trial crosses one, and that both are configurable. Mirrored in
18
+ `docs/using-cyborg-hunter.md`'s configuration table.
19
+ - Replay viewer: continuous whole-session playback, default on. Pressing
20
+ play now rolls through every trial in the recording as one continuous
21
+ video instead of stopping at each trial boundary; a "pause at trial
22
+ boundaries" toggle restores the previous per-trial-stop behavior. A
23
+ "Trial k of N" indicator tracks position across the session.
24
+
25
+ ### Changed
26
+ - Internal refactor, no behavior change: the report index renderer and its
27
+ three plot renderers (session-timeline, trajectories, typing-profile) are
28
+ each split into a pure core (string- or canvas-returning) plus a thin fs
29
+ wrapper; `buildViewerModel` and `getByPath` moved to pure shared modules.
30
+ `renderIndexHtml` also gained optional demo-mode opts (`imageSources`,
31
+ `inlineReplayModels`) for inline image/replay embedding, with a hash-sync
32
+ guard for the opaque-origin iframe the demo renders reports inside. Together
33
+ these let the live demo render reports and plots directly in the browser;
34
+ default (CLI) output is byte-identical.
35
+ - The live demo (https://konukcan.github.io/cyborg-hunter/) was remodeled
36
+ into a 13-step guided tour; not part of the npm package.
37
+
38
+ ### Fixed
39
+ - Raw mouse coordinates were persisted in every trial report regardless of the
40
+ documented off-by-default `collectForPostHoc.rawMouseTrack` toggle. Reports
41
+ now omit the raw track unless the toggle is enabled, in which case it
42
+ persists as `mouseTrack` (ingest maps it back to `mouseEvents`); the derived
43
+ `mouseMetrics` signal is unaffected either way.
44
+
45
+ ## [0.7.1] — 2026-07-29
46
+
47
+ ### Fixed
48
+ - The npm package now includes `dist/` — 0.7.0 shipped without it, so every
49
+ documented unpkg/script-tag URL returned 404.
50
+ - `cyborg-hunter --version` prints the version instead of "Unknown command".
51
+
52
+ ## [0.7.0] — 2026-07-14
53
+
54
+ ### Added
55
+ - Session replay recorder (`dist/cyborg-hunter-replay.js`, opt-in): captures pointer,
56
+ keys, clipboard, scroll, touch, viewport, and (at the `dom` tier) DOM snapshots +
57
+ mutations. Wire format is jsPsych's `SessionRecording v1` with a `ch_extensions`
58
+ namespace.
59
+ - CLI report gains a per-participant replay viewer (scrub bar, cursor trail, event
60
+ markers); `dom`-tier recordings reconstruct the page in a sandboxed iframe.
61
+ - Autosave and CLI ingest of replay artifacts, with ownership verification and
62
+ reload-collision handling.
63
+ - Guard-honeypot and guard-friction events are captured in the replay stream.
64
+
65
+ ### Changed
66
+ - Replay viewer uses a per-trial camera model for cursor/DOM alignment; legacy
67
+ recordings fall back to a clearly-labeled reduced-alignment mode.
68
+ - Password inputs are redacted unconditionally in replay capture; clipboard events
69
+ record lengths only, not content.
70
+
71
+ ### Fixed
72
+ - `drop` listener was incorrectly gated on the `paste` signal flag.
73
+ - Idle-gap and element-trace timers leaked per session instead of being trial-scoped.
74
+ - Edge-exit analysis silently found nothing on modern payloads due to a mismatched
75
+ time base.
76
+ - Shape-3 (top-level array) ingest dropped outer trial fields and skipped tab-away
77
+ normalization.
78
+ - Malformed replay artifacts no longer abort the report; `autoSave()` no longer
79
+ throws on circular/BigInt data.
80
+
81
+ ## [0.6.2] — 2026-07-12
82
+
83
+ ### Fixed
84
+ - Cut events now count toward the hard-copy screenout (previously recorded but
85
+ never incremented the session copy count).
86
+ - Misconfigured scoring overrides (e.g. a nested typo) now warn instead of
87
+ silently disabling a rule.
88
+ - Fullscreen detection is prefix-aware, fixing false violations on Safari <16.4
89
+ and some iOS WebViews.
90
+ - Honeypot re-initialization no longer inherits a prior run's violations/state.
91
+ - `decoyAnswer: false` per-trial opt-out is now honored (was coerced to `null`).
92
+ - A payload with both `trials` and `responses` no longer loses its integrity trials.
93
+ - A non-array signal field no longer crashes the report; ingest now coerces and warns.
94
+ - A numeric `trialId`/`ruleId` no longer crashes the trajectory renderer.
95
+ - `findGuardViolations()` now scans all trials instead of locking onto the first.
96
+
97
+ ### Added
98
+ - Warnings for an unresolved `participantIdField`, duplicate participant IDs, an
99
+ unmatched `phaseScope` phase name, and a non-numeric `scoring.softScoreThreshold`.
100
+
101
+ ## [0.6.1] — 2026-07-06
102
+
103
+ ### Added
104
+ - `endTrial()` stamps a wall-clock ISO `timestamp` on the trial report.
105
+ - Session-level `tabAwayEvents[]` alongside `tabAwaySums`, preserving full timing
106
+ for tab-aways outside `startTrial`/`endTrial`.
107
+ - `participantIdField` accepts dot-paths (e.g. `"metadata.sessionId"`).
108
+ - `sessionIntegrityPath`, `phaseScope`, and `trajectoryDisplayOrder` config options.
109
+ - `showPlatformId` flag (default off) to render a platform ID as a secondary line
110
+ in the HTML report.
111
+ - `--config-file` accepted as an alias of `--config`.
112
+
113
+ ### Changed
114
+ - `layoutShifts` renamed to `viewportWidthShifts` (old key kept as a deprecated
115
+ alias); the signal measures viewport-width changes, not Web Vitals CLS.
116
+ - Viewport-shift logging is debounced (250ms default) instead of firing per
117
+ animation frame.
118
+ - Trajectory panels are tinted by phase; `triage.md` carries an explicit Tier column.
119
+
120
+ ### Fixed
121
+ - `sessionIntegrityPath` no longer accepts a wrong-shaped object at the resolved
122
+ path, which had silently zeroed downstream signals.
123
+
124
+ ## [0.6.0] — 2026-06-25
125
+
126
+ No public API was removed. Re-running the report on existing data may shift
127
+ triage scores/ordering (sidebar, tab-away, and hard-flag corrections); newly
128
+ collected data no longer saves raw per-keystroke timings by default.
129
+
130
+ ### Changed
131
+ - Sidebar events are counted as distinct openings, not raw log entries.
132
+ - Hard-flag fallback now uses the cumulative session total against the count
133
+ threshold, not per-trial hits.
134
+ - Tab-away binning matches the runtime's strict cutoff.
135
+ - Triage ranks tier-first (hard → soft → clean), then by score.
136
+ - The CLI honors each participant's own saved thresholds instead of generic defaults.
137
+
138
+ ### Added
139
+ - Guard-honeypot self-disclosure surfaced in the report (`honeypot_ai_use`,
140
+ `honeypot_ai_report` columns).
141
+ - `finalize()` persists the runtime config so the CLI can reconstruct each
142
+ participant's screening settings.
143
+
144
+ ### Privacy
145
+ - Raw per-keystroke timings are no longer persisted by default
146
+ (`keystrokeDynamics` toggle); only derived typing speed is kept unless opted in.
147
+
148
+ ### Fixed
149
+ - Restored the authoritative session score for two saved formats that had been
150
+ dropped, causing over-flagging.
151
+ - Session-timeline offset estimation no longer drifts for legacy participants.
152
+
153
+ ## [0.5.1] and earlier
154
+
155
+ See the git tags `v0.4.0`, `v0.5.0`, `v0.5.1` for prior releases (no changelog
156
+ 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.1
29
+ date-released: '2026-07-29'
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
@@ -4,25 +4,31 @@ Detects AI-tool use during browser-based behavioral experiments. Captures paste,
4
4
 
5
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
- ### Example: 100-participant Prolific pilot
7
+ **[Try the live demo →](https://konukcan.github.io/cyborg-hunter/)** — run the tour in your browser; nothing leaves your machine.
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 |
9
+ ### Example: what a report looks like
15
10
 
16
- 6 hard-flagged + 12 soft-flagged out of 100. Per-participant tab-away timeline (rank 4, 42 events across 15 trials):
11
+ The bundled three-participant synthetic dataset (`examples/synthetic-pilot/` — every number hand-authored, no real participant behind any of it) triages like this:
17
12
 
18
- ![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)
13
+ | Rank | Participant | Tier | Score | Reason |
14
+ |------|-------------|------|-------|--------|
15
+ | 1 | SYN-HARD-03 | **HARD** | 18 | 2 paste events; 1 copy events; 3 tab-aways ≥10s; 2 flickers ≤3s; fast typing on 1 trials |
16
+ | 2 | SYN-SOFT-02 | soft | 21 | 3 copy events; 1 tab-away ≥10s; 2 tab-aways 3–10s; 1 sidebar event |
17
+ | 3 | SYN-CLEAN-01 | clean | 0 | 1 flicker ≤3s; 1 layout shifts |
18
+
19
+ 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:
20
+
21
+ ![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)
22
+
23
+ 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).
19
24
 
20
25
  ## Repo layout
21
26
 
22
27
  - `src/core/` — signal-collection library (the monitor)
23
28
  - `src/jspsych/` — jsPsych extension adapters (one per concern)
24
29
  - `src/cli/` + `bin/` — CLI that turns saved data into the triage report
25
- - `tests/`, `docs/`, `studies/` — tests, package docs, full-study fixtures
30
+ - `tests/`, `docs/` — tests, package docs
31
+ - `examples/synthetic-pilot/` — synthetic three-participant dataset for trying the CLI (see [docs/worked-example.md](docs/worked-example.md))
26
32
 
27
33
  ## Install
28
34
 
@@ -41,9 +47,11 @@ Browser (experiment page) — load via unpkg or copy `dist/*.js` into your proje
41
47
  <!-- Optional: deterrence + bait detection -->
42
48
  <script src="https://unpkg.com/cyborg-hunter/dist/extension-guard-friction.js"></script>
43
49
  <script src="https://unpkg.com/cyborg-hunter/dist/extension-guard-honeypot.js"></script>
50
+ <!-- Optional: session replay recorder -->
51
+ <script src="https://unpkg.com/cyborg-hunter/dist/cyborg-hunter-replay.js"></script>
44
52
  ```
45
53
 
46
- For production studies, pin a version: `https://unpkg.com/cyborg-hunter@0.4.0/dist/...`.
54
+ For production studies, pin a version: `https://unpkg.com/cyborg-hunter@0.7.1/dist/...`.
47
55
 
48
56
  ## Plug into a jsPsych experiment
49
57
 
@@ -83,7 +91,47 @@ cyborg-hunter report # writes ./cyborg-hunter-report/
83
91
  open cyborg-hunter-report/index.html
84
92
  ```
85
93
 
86
- Output: `summary.csv` (per-participant columns), `triage.md` (ranked list), `event-log.csv` (chronological events), `images/` (per-participant mouse paths, tab timelines, typing profiles), `index.html` (landing page).
94
+ 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).
95
+
96
+ ## Session replay
97
+
98
+ The optional replay recorder captures what the participant did and (at the
99
+ `dom` tier) what the page looked like, so a flagged session can be reviewed
100
+ visually instead of adjudicated from counts alone. Recordings use jsPsych
101
+ PR #3661's `SessionRecording v1` wire format with a `ch_extensions` block.
102
+
103
+ ```javascript
104
+ // jsPsych: one more extension (declare anywhere; finalize LAST)
105
+ { type: jsPsychCyborgHunterReplay, params: {
106
+ participantId: participant_id,
107
+ tier: 'dom', // 'trace' (default) | 'dom'
108
+ autoSave: { mode: 'datapipe', experimentId: 'ABC123' } } }
109
+ // on_finish: await jsPsych.extensions['cyborg-hunter-replay'].finalize();
110
+ ```
111
+
112
+ ```javascript
113
+ // Standalone (any experiment, no jsPsych)
114
+ const rec = CyborgHunterReplay.attach({ participantId, tier: 'dom',
115
+ autoSave: { mode: 'datapipe', experimentId: 'ABC123' } });
116
+ rec.startSession();
117
+ rec.startTrial({ trialId: 'r1' }); // optional bracketing
118
+ rec.endTrial();
119
+ rec.stopSession('finished');
120
+ await rec.autoSaveNow();
121
+ ```
122
+
123
+ The artifact saves as `<pid>-replay-<epoch>.json` next to your data; the CLI
124
+ picks it up automatically and the report gains a **Session replay** section
125
+ per participant (lazy-loaded scrub viewer — cursor trail, clicks, away
126
+ bands, and a sandboxed reconstruction of the page for `dom`-tier
127
+ recordings). On `dom`-tier recordings the cursor is verified per
128
+ interaction by a five-way alignment self-check — any click it can't
129
+ confirm draws an explicit uncertain marker instead of a wrong one, and
130
+ recordings made before this guarantee existed replay under a reduced-
131
+ guarantees banner. Password fields are always redacted; see
132
+ [docs/using-cyborg-hunter.md](docs/using-cyborg-hunter.md) for the privacy
133
+ model, data-volume guidance, delivery semantics, and the alignment
134
+ guarantee in full.
87
135
 
88
136
  ## What it detects
89
137
 
@@ -95,16 +143,18 @@ Output: `summary.csv` (per-participant columns), `triage.md` (ranked list), `eve
95
143
  | Tab-away | `visibilitychange` + `blur`/`focus` | Soft (weighted) |
96
144
  | Browser sidebar | `innerWidth` delta + layout compression | Soft (weighted) |
97
145
  | Suspicious typing speed | chars/sec exceeding preset threshold | Soft (weighted) |
98
- | Synthetic insertion | text appearing without keystrokes | Soft (weighted) |
146
+ | Synthetic insertion | text appearing without keystrokes | Diagnostic (collected, not scored) |
99
147
  | Foreign input | typing landing outside experiment container | Soft (weighted) |
100
- | Idle gaps | input inactivity | Soft (weighted) |
101
- | AI-extension content scripts | DOM scan for known extension selectors | Soft (weighted) |
148
+ | Idle gaps | input inactivity | Diagnostic (collected, not scored) |
149
+ | AI-extension content scripts | DOM scan for known extension selectors | Diagnostic (collected, not scored) |
102
150
  | Mouse trajectories | 20Hz polling + path-efficiency metrics | Diagnostic |
103
151
  | Window/screen geometry | polled + resize-event capture, with zoom inference | Diagnostic |
104
152
 
105
153
  Three presets: `permissive` / `standard` (default) / `strict`. Per-signal thresholds: [docs/signals-reference.md](docs/signals-reference.md).
106
154
 
107
- 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) — captured as `ai_use` / `ai_report` columns.
155
+ 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.)
156
+
157
+ 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).
108
158
 
109
159
  ## What it doesn't detect
110
160
 
@@ -114,21 +164,30 @@ The optional **guard** extensions add: fullscreen / sidebar / focus enforcement
114
164
 
115
165
  ## Documentation
116
166
 
167
+ - [docs/quickstart.md](docs/quickstart.md) — zero to triage report
168
+ - [docs/worked-example.md](docs/worked-example.md) — full pipeline run on the bundled synthetic dataset, outputs interpreted
169
+ - [docs/interpreting-signals.md](docs/interpreting-signals.md) — scores vs tiers, viewport shifts, phase scoping: the common misreadings
170
+ - [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
171
+ - [docs/release-notes-0.6.1.md](docs/release-notes-0.6.1.md) — the 0.6.1 release notes
117
172
  - [docs/using-cyborg-hunter.md](docs/using-cyborg-hunter.md) — full integration guide
118
173
  - [docs/signals-reference.md](docs/signals-reference.md) — every signal with thresholds per preset
119
174
  - [docs/configuration.md](docs/configuration.md) — config file fields and CLI flags
120
175
  - [docs/cli-reference.md](docs/cli-reference.md) — commands and output structure
121
- - [docs/design-decisions.md](docs/design-decisions.md) — architecture rationale
176
+
177
+ ## Development
178
+
179
+ 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.
122
180
 
123
181
  ## Citation
124
182
 
125
183
  ```bibtex
126
- @software{konuk2026cyborghunter,
127
- author = {Konuk, Can},
128
- title = {cyborg-hunter: integrity monitoring for online behavioral experiments},
184
+ @software{konuk_cyborg_hunter,
185
+ author = {Konuk, Can and Btesh, Victor and Nunez, Jose Luis},
186
+ title = {cyborg-hunter: detecting AI-tool use in browser-based behavioral experiments},
129
187
  year = {2026},
188
+ version = {0.7.1},
130
189
  url = {https://github.com/konukcan/cyborg-hunter},
131
- version = {0.4.0}
190
+ license = {MIT}
132
191
  }
133
192
  ```
134
193
 
@@ -4,17 +4,21 @@
4
4
  // npx cyborg-hunter init # generate starter config
5
5
  // npx cyborg-hunter report [--config path] ... # generate report
6
6
 
7
+ import { readFileSync } from 'node:fs';
7
8
  import { run } from '../src/cli/report.js';
8
9
  import { runInit } from '../src/cli/init.js';
9
10
 
10
11
  const args = process.argv.slice(2);
11
12
  const subcommand = args[0];
12
13
 
13
- if (subcommand === 'init') {
14
+ if (subcommand === '--version' || subcommand === '-v') {
15
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
16
+ console.log(pkg.version);
17
+ } else if (subcommand === 'init') {
14
18
  runInit().catch(err => { console.error('Error:', err.message); process.exit(1); });
15
19
  } else if (subcommand === 'report' || !subcommand) {
16
20
  run(args).catch(err => { console.error('Error:', err.message); process.exit(1); });
17
21
  } else {
18
- console.error(`Unknown command: ${subcommand}. Use "report" or "init".`);
22
+ console.error(`Unknown command: ${subcommand}. Use "report", "init", or "--version".`);
19
23
  process.exit(1);
20
24
  }
@@ -0,0 +1,3 @@
1
+ // cyborg-hunter-replay v0.7.2 — https://github.com/konukcan/cyborg-hunter
2
+ (()=>{var K={participantId:"unknown",tier:"trace",keys:"full",mouseHz:30,redactSelector:"[data-ch-redact]",keepBait:!1,root:null,autoSave:{mode:"none"},maxEventsPerTrial:5e4,maxCharsPerTrial:8e6},ye={created:["session","destroyed"],session:["trial","stopped","destroyed"],trial:["session","stopped","destroyed"],stopped:["destroyed"],destroyed:[]};function Z(e){var n=Object.assign({},K,e||{});n.autoSave=Object.assign({},K.autoSave,(e||{}).autoSave||{});var r="created",t=[],a=[],l=0,f=[],c=new WeakMap,d=new WeakSet;function y(s){try{return JSON.stringify(s).length}catch{return 0}}var p={participantId:n.participantId,tier:n.tier,keys:n.keys,sessionStart:null,sessionStartEpoch:null,viewport:null,stylesheets:[],trials:[],guardViolations:[],captureFailures:[],captureStopped:!1,endReason:null,markerAttr:null},h=null,u=null;function v(s){if(ye[r].indexOf(s)===-1)throw new Error("[cyborg-hunter-replay] invalid lifecycle call: "+r+" \u2192 "+s+". Expected order: attach() \u2192 startSession() \u2192 (startTrial \u2192 endTrial)* \u2192 stopSession() \u2192 destroy()."+(r==="created"?" Did you forget startSession()?":""));r=s}function T(s,m){return l++,{trialIndex:l-1,trialId:s&&s.trialId||(m?"__session__":"trial-"+(l-1)),plugin:s&&s.plugin||"ch:standalone",implicit:!!m,tLoad:m?p.sessionStart:performance.now(),tStart:(s&&s.tStart)!=null?s.tStart:null,tDomReady:(s&&s.tDomReady)!=null?s.tDomReady:null,tEnd:null,initialDom:"",events:[]}}function E(){h.tEnd=performance.now(),p.trials.push(h),h=null}function x(s){for(var m=0;m<f.length;m++)try{f[m](s)}catch(_){p.captureFailures.push({channel:"trial_start_hook",message:_&&_.message?String(_.message):String(_),t:performance.now()})}}var P={config:n,startSession:function(){v("session"),p.sessionStart=performance.now(),p.sessionStartEpoch=Date.now();var s=typeof window<"u"?window:null,m=typeof document<"u"&&document.documentElement?document.documentElement:null;p.viewport=s?{width:s.innerWidth||null,height:s.innerHeight||null,client_width:m&&m.clientWidth||null,client_height:m&&m.clientHeight||null,dpr:s.devicePixelRatio||1,visual_viewport:s.visualViewport?{width:s.visualViewport.width,height:s.visualViewport.height,scale:s.visualViewport.scale}:null}:{width:null,height:null,client_width:null,client_height:null,dpr:null,visual_viewport:null},n.autoSave.mode==="none"&&console.warn('[cyborg-hunter-replay] autoSave.mode is "none" \u2014 the recording will be lost unless you call getRecording() yourself.')},startTrial:function(s){r==="trial"?(this.pushEvent("ch:lifecycle_error",{reason:"startTrial_without_endTrial"}),E(),r="session"):h&&E(),v("trial"),h=T(s,!1),x(h)},endTrial:function(){v("session"),h&&E()},stopSession:function(s){r==="trial"&&(r="session"),h&&E(),v("stopped"),p.endReason=s||"finished"},pushEvent:function(s,m,_){if(r==="destroyed")throw new Error("[cyborg-hunter-replay] pushEvent called on a destroyed recorder");if(!(r==="created"||r==="stopped")&&(h||(h=T(null,!0),x(h)),!d.has(h))){if(h.events.length>=n.maxEventsPerTrial){d.add(h),p.captureStopped=!0,h.events.push({t:_??performance.now(),kind:"ch:capture_stopped",limit:n.maxEventsPerTrial});return}var b=Object.assign({t:_??performance.now(),kind:s},m||{});if(n.maxCharsPerTrial!=null){var M=c.get(h);if(M==null&&(M=h.initialDom?h.initialDom.length:0),M+=y(b),M>n.maxCharsPerTrial){d.add(h),p.captureStopped=!0,h.events.push({t:b.t,kind:"ch:capture_stopped",limit_chars:n.maxCharsPerTrial});return}c.set(h,M)}h.events.push(b)}},captureFailure:function(s,m){p.captureFailures.push({channel:s,message:m&&m.message?String(m.message):String(m),t:performance.now()})},onTrialStart:function(s){f.push(s)},setStylesheets:function(s){p.stylesheets=s||[]},setMarkers:function(s){u=s},getMarkers:function(){return u},setMarkerAttr:function(s){p.markerAttr=s||null},addListener:function(s,m,_,b){s.addEventListener(m,_,b||!1),t.push({target:s,event:m,handler:_,options:b||!1})},addInterval:function(s){a.push(s)},getState:function(){var s=p.trials.slice();return h&&s.push(h),Object.assign({},p,{trials:s,state:r})},destroy:function(){r!=="destroyed"&&(v("destroyed"),t.forEach(function(s){s.options&&s.options._isObserver?s.handler.disconnect():s.target.removeEventListener(s.event,s.handler,s.options)}),t=[],a.forEach(function(s){clearInterval(s)}),a=[])}};return P}function w(e,n,r){return function(t){try{r(t)}catch(a){e.captureFailure(n,a)}}}function $(e){if(!e||!e.tagName)return null;var n=e.tagName.toLowerCase();return e.id&&(n+="#"+e.id),n}function we(e){return!!(e&&e.tagName==="INPUT"&&e.type==="password")}function Se(e,n){if(we(e))return!0;if(!n||!e||typeof e.matches!="function")return!1;try{return e.matches(n)}catch{return!1}}function N(e,n){for(var r=e;r;){if(Se(r,n))return!0;r=r.parentNode}return!1}function k(e){return Math.round(e*10)/10}function ee(e,n){n=n||{};var r=n.doc||document,t=n.win||window,a=n.now||function(){return performance.now()},l=n.raf||(typeof requestAnimationFrame<"u"?requestAnimationFrame.bind(null):function(i){setTimeout(i,16)}),f=e.config;function c(){var i=r.documentElement;return{cw:i&&i.clientWidth?i.clientWidth:t.innerWidth||null,ch:i&&i.clientHeight?i.clientHeight:t.innerHeight||null}}function d(i,o){return o.clientX!=null&&(i.cx=Math.round(o.clientX)),o.clientY!=null&&(i.cy=Math.round(o.clientY)),i}function y(i){var o=i&&i.target;if(!o||!o.tagName)return null;var g=o.tagName.toLowerCase();if(N(o,f.redactSelector))return{redacted:!0,tag:g};var S={tag:g};try{if(typeof i.composedPath=="function"){var A=i.composedPath()[0];A&&A!==o&&(S.shadow=!0)}}catch{}o.id&&(S.id=o.id);var Q=typeof e.getMarkers=="function"?e.getMarkers():null;Q&&(S.n=Q.refFor(o));try{if(typeof o.getBoundingClientRect=="function"){var D=o.getBoundingClientRect();S.rect=[k(D.left),k(D.top),k(D.width),k(D.height)]}}catch{}return S}var p=null,h=new Map,u=null,v=null,T=!1,E=!1,x=!1;function P(){T=!1,p&&(e.pushEvent("scroll",{x:Math.round(t.scrollX||0),y:Math.round(t.scrollY||0)},p.t),p=null),h.size>0&&(h.forEach(function(i,o){var g;if(N(o,f.redactSelector))g={redacted:!0,tag:o.tagName?o.tagName.toLowerCase():null,x:Math.round(o.scrollLeft||0),y:Math.round(o.scrollTop||0)};else{g={el:$(o),id:o.id||null,x:Math.round(o.scrollLeft||0),y:Math.round(o.scrollTop||0)};var S=typeof e.getMarkers=="function"?e.getMarkers():null;S&&(g.n=S.refFor(o))}e.pushEvent("scroll",g,i.t)}),h.clear())}function s(i){return i}function m(){if(E=!1,!!u){var i=c();e.pushEvent("resize",{w:t.innerWidth,h:t.innerHeight,cw:i.cw,ch:i.ch,dpr:t.devicePixelRatio||1},u.t),u=null}}function _(){if(x=!1,!!v){var i=t.visualViewport;i&&e.pushEvent("vv",{w:k(i.width),h:k(i.height),scale:i.scale,px:k(i.pageLeft||0),py:k(i.pageTop||0)},v.t),v=null}}function b(){P(),m(),_()}function M(i){var o=c();return i.sx=Math.round(t.scrollX||0),i.sy=Math.round(t.scrollY||0),i.vw=t.innerWidth||null,i.vh=t.innerHeight||null,i.cw=o.cw,i.ch=o.ch,i}var ve=1e3/(f.mouseHz||30),J=-1/0;e.addListener(r,"mousemove",w(e,"mouse",function(i){var o=a();o-J<ve||(J=o,e.pushEvent("mousemove",d({x:Math.round(i.pageX),y:Math.round(i.pageY)},i),o))}),{passive:!0,capture:!0}),["mousedown","mouseup","click"].forEach(function(i){e.addListener(r,i,w(e,"mouse",function(o){b();var g=M(d({x:Math.round(o.pageX),y:Math.round(o.pageY)},o)),S=y(o);S&&(g.target=S),e.pushEvent(i,g,a())}),{passive:!0,capture:!0})}),f.keys!=="off"&&["keydown","keyup"].forEach(function(i){e.addListener(r,i,w(e,"keys",function(o){if(N(o.target,f.redactSelector)){e.pushEvent(i,{redacted:!0},a());return}e.pushEvent(i,{key:o.key,code:o.code},a())}),!0)}),e.addListener(r,"paste",w(e,"clipboard",function(i){b();var o=null;try{o=i.clipboardData.getData("text").length}catch{}e.pushEvent("paste",{len:o},a())}),!0),e.addListener(r,"copy",w(e,"clipboard",function(){b(),e.pushEvent("copy",{},a())}),!0),e.addListener(r,"cut",w(e,"clipboard",function(){b(),e.pushEvent("cut",{},a())}),!0),e.addListener(r,"drop",w(e,"clipboard",function(i){b();var o=null;try{o=i.dataTransfer.getData("text").length}catch{}e.pushEvent("ch:drop",{len:o},a())}),!0);var F=new Map,z=!1;function me(){z=!1,F.forEach(function(i,o){var g={el:$(o),id:o.id||null},S=o.value!=null?String(o.value):o.isContentEditable&&o.textContent!=null?String(o.textContent):"";if(N(o,f.redactSelector))g.redacted=!0,g.value_len=S.length;else{g.value=S;var A=typeof e.getMarkers=="function"?e.getMarkers():null;A&&(g.n=A.refFor(o))}!g.redacted&&(o.type==="checkbox"||o.type==="radio")&&(g.checked=!!o.checked),e.pushEvent("input",g,i)}),F.clear()}e.addListener(r,"input",w(e,"input",function(i){i.target&&(F.set(i.target,a()),z||(z=!0,l(w(e,"input",me))))}),!0),e.addListener(t,"scroll",w(e,"scroll",function(i){var o=i&&i.target&&i.target.tagName?i.target:null;o?h.set(o,{t:a()}):p={t:a()},T||(T=!0,l(w(e,"scroll",P)))}),{passive:!0,capture:!0}),e.addListener(r,"touchstart",w(e,"touch",function(i){b();var o=i.touches&&i.touches[0];if(o){var g=M(d({x:Math.round(o.pageX),y:Math.round(o.pageY)},o)),S=y(i);S&&(g.target=S),e.pushEvent("touchstart",g,a())}}),{passive:!0,capture:!0});var j=!1,L=null,q=0;e.addListener(r,"touchmove",w(e,"touch",function(i){L=i.touches&&i.touches[0],q=a(),!j&&(j=!0,l(w(e,"touch",function(){j=!1,L&&e.pushEvent("touchmove",d({x:Math.round(L.pageX),y:Math.round(L.pageY)},L),q)})))}),{passive:!0,capture:!0}),e.addListener(r,"touchend",w(e,"touch",function(i){b();var o=i.changedTouches&&i.changedTouches[0];if(o){var g=M(d({x:Math.round(o.pageX),y:Math.round(o.pageY)},o)),S=y(i);S&&(g.target=S),e.pushEvent("touchend",g,a())}}),{passive:!0,capture:!0}),e.addListener(t,"blur",w(e,"focus",function(){e.pushEvent("blur",{},a())})),e.addListener(t,"focus",w(e,"focus",function(){e.pushEvent("focus",{},a())})),e.addListener(r,"visibilitychange",w(e,"focus",function(){e.pushEvent("visibility",{hidden:!!r.hidden},a())})),e.addListener(t,"resize",w(e,"viewport",function(){u={t:a()},E||(E=!0,l(w(e,"viewport",m)))})),t.visualViewport&&typeof t.visualViewport.addEventListener=="function"&&["resize","scroll"].forEach(function(i){e.addListener(t.visualViewport,i,w(e,"viewport",function(){v={t:a()},x||(x=!0,l(w(e,"viewport",_)))}))}),e.onTrialStart(function(i){var o=c();i.viewState={x:Math.round(t.scrollX||0),y:Math.round(t.scrollY||0),w:t.innerWidth||null,h:t.innerHeight||null,cw:o.cw,ch:o.ch,dpr:t.devicePixelRatio||1}})}var O=1,_e=3,be={SCRIPT:!0,NOSCRIPT:!0};function Te(e){var n=new WeakMap,r=1;return{attr:"data-chn-"+e,refFor:function(t){var a=n.get(t);return a==null&&(a=r++,n.set(t,a)),a}}}var Ee={AREA:!0,BASE:!0,BR:!0,COL:!0,EMBED:!0,HR:!0,IMG:!0,INPUT:!0,LINK:!0,META:!0,SOURCE:!0,TRACK:!0,WBR:!0},xe=/^[a-zA-Z_:][-a-zA-Z0-9_:.]*$/;function ae(e){return xe.test(e)&&!/^on/i.test(e)}function G(e){return String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function ie(e,n){if(n&&n.keepBait||!e||e.nodeType!==O)return!1;if(e.id==="ch-decoy")return!0;for(var r=e.attributes||[],t=0;t<r.length;t++)if(r[t].name==="data-ch-role"||r[t].name==="data-ch-decoy")return!0;return!1}function Me(e){return e.tagName==="INPUT"&&(e.type==="password"||H(e,"type","password"))}function oe(e,n){if(!e)return!1;if(Me(e))return!0;var r=n&&n.redactSelector;if(!r||typeof e.matches!="function")return!1;try{return e.matches(r)}catch{return!1}}function V(e,n){for(var r=e;r;){if(r.nodeType===O&&oe(r,n))return!0;r=r.parentNode}return!1}function H(e,n,r){for(var t=e.attributes||[],a=0;a<t.length;a++)if(t[a].name===n)return r==null||t[a].value===r;return!1}function B(e,n){n=n||{};var r=[];return U(e,n,r),r.join("")}function Re(e){var n=null,r=null;try{if(typeof e.getBoundingClientRect=="function"){var t=e.getBoundingClientRect();t&&(n=Math.round(t.width||0),r=Math.round(t.height||0))}}catch{}return n==null&&(n=parseInt(H(e,"width")?re(e,"width"):"",10),r=parseInt(H(e,"height")?re(e,"height"):"",10),n>0||(n=300),r>0||(r=150)),{w:n,h:r}}var te=["position","top","right","bottom","left","margin","float","vertical-align","z-index"];function se(e){var n=Re(e),r="",t="inline-block";try{var a=e.ownerDocument&&e.ownerDocument.defaultView;if(a&&typeof a.getComputedStyle=="function"){var l=a.getComputedStyle(e);l.display&&l.display!=="inline"&&(t=l.display);for(var f=0;f<te.length;f++){var c=te[f],d=l.getPropertyValue(c);d&&d!=="auto"&&d!=="none"&&d!=="normal"&&d!=="baseline"&&d!=="0px"&&(r+=c+":"+d+";")}}}catch{}return"display:"+t+";"+r+"width:"+n.w+"px;height:"+n.h+"px"}function Pe(e,n,r){r.push('<span data-ch-iframe=""'),n.markers&&r.push(" "+n.markers.attr+'="'+n.markers.refFor(e)+'"'),r.push(' style="'+se(e)+'"></span>')}function re(e,n){for(var r=e.attributes||[],t=0;t<r.length;t++)if(r[t].name===n)return r[t].value;return""}function U(e,n,r){if(e){if(e.nodeType===_e){r.push(G(e.textContent||""));return}if(e.nodeType===O){var t=e.tagName;if(!be[t]&&!ie(e,n)){if(t==="IFRAME"){Pe(e,n,r);return}var a=t.toLowerCase();r.push("<"+a),n.markers&&r.push(" "+n.markers.attr+'="'+n.markers.refFor(e)+'"'),e.shadowRoot&&r.push(' data-ch-shadow=""');for(var l=oe(e,n),f=t==="IMG"&&e.src?e.src:null,c=!1,d=e.attributes||[],y=0;y<d.length;y++){var p=d[y].name,h=d[y].value;ae(p)&&(p==="value"&&l||(p==="src"&&f&&(h=f,c=!0),r.push(" "+p+'="'+G(h)+'"')))}if(f&&!c&&r.push(' src="'+G(f)+'"'),l&&r.push(' data-ch-redacted="true"'),r.push(">"),!Ee[t]){if(!l)for(var u=e.childNodes||[],v=0;v<u.length;v++)U(u[v],n,r);r.push("</"+a+">")}}}}}function ne(e,n){for(var r=[],t=e;t&&t!==n;){var a=t.parentNode;if(!a)return null;var l=ke(a,t);if(l===-1)return null;r.unshift(l),t=a}return t===n?r:null}function ke(e,n){for(var r=e.childNodes||[],t=0;t<r.length;t++)if(r[t]===n)return t;return-1}function Ce(e,n,r){for(var t=e.target,a=t;a;){if(ie(a,r))return null;a=a.parentNode}if(e.type==="characterData"&&r.markers){var l=t.parentNode;if(!l||ne(l,n)===null)return null;e={type:"childList",target:l},t=l}var f=ne(t,n);if(f===null)return null;var c=r.markers&&t.nodeType===O?{n:r.markers.refFor(t),tag:t.tagName==="IFRAME"?"span":t.tagName.toLowerCase()}:null;if(e.type==="childList")return Object.assign({op:"childList",path:f,html:V(t,r)?"":Ie(t,r)},c||{});if(e.type==="attributes"){if(t.tagName==="IFRAME")return Object.assign({op:"attributes",path:f,name:"style",value:se(t)},c||{});var d=e.attributeName;if(!ae(d))return null;var y=typeof t.getAttribute=="function"?t.getAttribute(d):null;return d==="value"&&V(t,r)&&(y=null),Object.assign({op:"attributes",path:f,name:d,value:y},c||{})}if(e.type==="characterData"){var p=V(t,r)?"":t.textContent||"";return{op:"characterData",path:f,value:p}}return null}function Ie(e,n){for(var r=[],t=e.childNodes||[],a=0;a<t.length;a++)U(t[a],n,r);return r.join("")}function Le(e){for(var n=[],r=e&&e.styleSheets||[],t=0;t<r.length;t++){var a=r[t];try{for(var l=a.cssRules,f=[],c=0;c<l.length;c++)f.push(l[c].cssText);n.push({css:f.join(`
3
+ `)})}catch{n.push({href:a.href||null})}}return n}function ue(e,n){n=n||{};var r=n.doc||document,t=n.win||window,a=n.now||function(){return performance.now()},l=n.MutationObserver||(typeof MutationObserver<"u"?MutationObserver:null),f=Te(Math.random().toString(16).slice(2,10)||"fallback0");typeof e.setMarkers=="function"&&(e.setMarkers(f),e.setMarkerAttr(f.attr));var c={keepBait:e.config.keepBait,redactSelector:e.config.redactSelector,markers:f};function d(){var u=e.config.root;if(typeof u=="string")try{return r.querySelector(u)||r.body}catch{return r.body}return u||r.body}try{e.setStylesheets(Le(r))}catch(u){e.captureFailure("stylesheets",u)}if(e.onTrialStart(function(u){try{var v=B(d(),c),T=e.config.maxCharsPerTrial;T!=null&&v.length>T?(e.captureFailure("dom_snapshot",new Error("initial DOM snapshot length "+v.length+" exceeds maxCharsPerTrial "+T+"; dropped to bound payload size")),u.initialDom=""):u.initialDom=v}catch(E){e.captureFailure("dom_snapshot",E),u.initialDom=""}}),l){var y=new l(function(u){try{for(var v=d(),T=new Set,E=function(m){return m.type==="childList"?m.target:m.type==="characterData"&&c.markers&&m.target&&m.target.parentNode?m.target.parentNode:null},x=0;x<u.length;x++){var P=E(u[x]);if(P){if(T.has(P))continue;T.add(P)}var s=Ce(u[x],v,c);s&&e.pushEvent("mutation",s,a())}}catch(m){e.captureFailure("mutations",m)}});try{y.observe(d(),{childList:!0,attributes:!0,characterData:!0,subtree:!0}),e.addListener({addEventListener:function(){},removeEventListener:function(){}},"_mutation_observer",y,{_isObserver:!0})}catch(u){e.captureFailure("mutations",u)}}if(t.GuardFriction&&typeof t.GuardFriction.onViolation=="function")try{var p=e.getState().state;if((p==="session"||p==="trial")&&typeof t.GuardFriction.getCurrentState=="function"){var h=t.GuardFriction.getCurrentState();h&&h.in_violation&&e.pushEvent("ch:guard_violation",{reason:h.current_reason||"unknown",phase:"start",synthesized_at_subscribe:!0,dom_at_subscribe:B(d(),c)},a())}t.GuardFriction.onViolation(function(u){try{u&&u.phase==="start"?e.pushEvent("ch:guard_violation",{reason:u.reason,phase:"start",pre_scramble_dom:B(d(),c)},a()):u&&u.phase&&e.pushEvent("ch:guard_violation",{reason:u.reason,phase:u.phase,duration_ms:u.duration!=null?u.duration:null},a())}catch(v){e.captureFailure("guard_violation",v)}})}catch(u){e.captureFailure("guard_violation",u)}}var W="0.7.2";function le(e){return String(e).replace(/[^a-zA-Z0-9_.-]/g,"_")}function C(e,n){return e==null?null:Math.round((e-n)*10)/10}function R(e,n){return(e||[]).map(function(r){var t=Object.assign({},r);return typeof t.t=="number"&&(t.t=C(t.t,n)),typeof t.start=="number"&&(t.start=C(t.start,n)),t})}function ce(e,n){n=n||{};var r=e.sessionStart;if(r==null)throw new Error("[cyborg-hunter-replay] cannot serialize before startSession() \u2014 call startSession() first, or the timestamps would be meaningless (Unix epoch).");var t=n.chSessionReport||null,a=r,l=e.trials.map(function(c){var d=c.events.slice().sort(function(p,h){return p.t-h.t}),y=d.length>0?d[d.length-1].t:c.tLoad;return y>a&&(a=y),{trial_index:c.trialIndex,trial_id:c.trialId,plugin:c.plugin,t_start:C(c.tStart,r),t_dom_ready:C(c.tDomReady,r),t_load:C(c.tLoad,r),t_end:C(c.tEnd!=null?c.tEnd:y,r),view_state:c.viewState||null,initial_dom:c.initialDom||"",events:d.map(function(p){var h=Object.assign({},p);return h.t=C(p.t,r),h}),trial_data:{}}}),f={replay_version:W,marker_attr:e.markerAttr||null,ch_version:t&&t.libraryVersion||null,preset:t&&t.config?t.config.preset:null,scoring:t?{hard_score:t.hardScore||{},soft_score:t.softScore!=null?t.softScore:null,soft_score_threshold:t.softScoreThreshold!=null?t.softScoreThreshold:null,any_hard_triggered:!!t.anyHardTriggered,trials_completed:t.trialsCompleted!=null?t.trialsCompleted:null}:null,session_signals:t?{sidebar_events:R(t.sidebarEvents,r),ai_extensions_found:R(t.aiExtensionsFound,r),devtools_events:R(t.devToolsEvents,r),window_positions:R(t.windowPositions,r),layout_shifts:R(t.layoutShifts,r),zoom_changes:R(t.zoomChanges,r),extension_injections:R(t.extensionInjections,r)}:null,guard_violations:R(e.guardViolations,r),capture_failures:R(e.captureFailures,r),capture_stopped:!!e.captureStopped};return{schema_version:1,metadata:{start_time:new Date(e.sessionStartEpoch).toISOString(),end_time:new Date(e.sessionStartEpoch+Math.max(0,Math.round(a-r))).toISOString(),end_reason:e.endReason||"aborted",participant_id:e.participantId,recorder:"cyborg-hunter-replay@"+W,tier:e.tier,keys:e.keys},viewport:e.viewport,stylesheets:{initial:e.stylesheets||[],events:[]},rng_calls:[],trials:l,ch_extensions:f}}var Ae="https://pipe.jspsych.org/api/data/";function de(e){var n=le(e.metadata.participant_id),r=Date.parse(e.metadata.start_time);return n+"-replay-"+r+".json"}function he(e,n){return{schema_version:e.schema_version,tier:e.metadata.tier,saved_to:n,bytes_uncompressed:JSON.stringify(e).length,capture_failures:(e.ch_extensions.capture_failures||[]).map(function(r){return r.channel}),capture_stopped:!!e.ch_extensions.capture_stopped}}async function fe(e,n){var r=n||{mode:"none"},t=de(e),a=typeof window<"u"?window:{};if(r.mode==="none")return{saved_to:"none",filename:t};if(r.mode==="datapipe"){if(!r.experimentId)return{saved_to:"failed",filename:t,error:'autoSave.mode "datapipe" requires autoSave.experimentId'};try{var l=JSON.stringify(e);if(a.jsPsychPipe&&typeof a.jsPsychPipe.saveData=="function")await a.jsPsychPipe.saveData(r.experimentId,t,l);else{var f=a.fetch||(typeof fetch<"u"?fetch:null);if(!f)throw new Error("no fetch available for DataPipe POST");var c=await f(Ae,{method:"POST",headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify({experimentID:r.experimentId,filename:t,data:l})});if(c&&c.ok===!1)throw new Error("DataPipe HTTP error")}return{saved_to:"datapipe:"+r.experimentId+"/"+t,filename:t}}catch(v){return console.error("[cyborg-hunter-replay] DataPipe autosave failed:",v),{saved_to:"failed",filename:t,error:v&&v.message?v.message:String(v)}}}if(r.mode==="download")try{var d=typeof document<"u"?document:null,y=a.Blob||(typeof Blob<"u"?Blob:null),p=a.URL||(typeof URL<"u"?URL:null);if(!d||!y||!p)throw new Error("download mode needs a browser");var h=new y([JSON.stringify(e)],{type:"application/json"}),u=d.createElement("a");return u.href=p.createObjectURL(h),u.download=t,d.body.appendChild(u),u.click(),d.body.removeChild(u),p.revokeObjectURL(u.href),{saved_to:"download",filename:t}}catch(v){return console.error("[cyborg-hunter-replay] download autosave failed:",v),{saved_to:"failed",filename:t,error:v&&v.message?v.message:String(v)}}return{saved_to:"failed",filename:t,error:'unknown autoSave.mode "'+r.mode+'"'}}async function pe(e){var n=JSON.stringify(e),r=typeof window<"u"?window:{},t=r.CompressionStream||(typeof CompressionStream<"u"?CompressionStream:null),a=r.Blob||(typeof Blob<"u"?Blob:null);if(!a)return null;if(!t)return new a([n],{type:"application/json"});var l=new a([n]).stream().pipeThrough(new t("gzip")),f=r.Response||(typeof Response<"u"?Response:null),c=await new f(l).arrayBuffer();return new a([c],{type:"application/gzip"})}var I=null;function Y(e){I&&(console.warn("[cyborg-hunter-replay] attach() called while a recorder is active \u2014 replacing the previous instance (its unsaved recording is discarded)."),I.destroy(),I=null);var n=Z(e),r={_recorder:n,config:n.config,startSession:function(){return n.startSession(),ee(n),(n.config.tier==="dom"||n.config.tier==="canvas")&&ue(n),r},startTrial:function(t){return n.startTrial(t),r},endTrial:function(){return n.endTrial(),r},stopSession:function(t){return n.stopSession(t),r},getRecording:function(t){return ce(n.getState(),t||{})},getRecordingCompressed:function(t){return pe(r.getRecording(t))},autoSaveNow:async function(t){if(n.getState().sessionStart==null)return{recording:null,saveResult:{saved_to:"no_session"},meta:{schema_version:1,saved_to:"no_session",note:"recorder was never started; nothing to save"}};var a=r.getRecording(t),l=await fe(a,n.config.autoSave),f=he(a,l.saved_to);return l.error&&(f.save_error=l.error),{recording:a,saveResult:l,meta:f}},destroy:function(){n.destroy(),I===r&&(I=null)}};return I=r,r}typeof window<"u"&&(window.CyborgHunterReplay={attach:Y});var X=class{static info={name:"cyborg-hunter-replay",version:"0.7.2",data:{}};constructor(n){this.jsPsych=n,this.api=null,this._chMonitor=null,this._lastRecording=null,this._monitoring=!1}initialize(n){this.params=n||{},this.api=Y(this.params),this.api.startSession(),this._stashChMonitor()}_stashChMonitor(){try{var n=this.jsPsych&&this.jsPsych.extensions&&this.jsPsych.extensions["cyborg-hunter"];n&&n.monitor&&(this._chMonitor=n.monitor)}catch{}}on_start(n){}on_load(n){if(this.api){this._monitoring=!0;var r=0,t="unknown";try{r=this.jsPsych.getProgress().current_trial_global}catch{}try{t=(this.jsPsych.getCurrentTrial()||{}).type?.info?.name||"unknown"}catch{}this.api.startTrial({trialId:n&&n.trialId||"trial-"+r,plugin:t})}}on_finish(n){return this.api&&this._monitoring&&(this._monitoring=!1,this.api.endTrial()),{}}async finalize(){if(this.api){try{try{this.api.stopSession("finished")}catch{}this._chMonitor||this._stashChMonitor();var n=null;try{n=this._chMonitor?this._chMonitor.getSessionReport():null}catch(t){console.warn("[cyborg-hunter-replay] could not pull CH session report:",t)}var r=await this.api.autoSaveNow({chSessionReport:n});this._lastRecording=r.recording,this.jsPsych.data.addProperties({integrityReplayMeta:r.meta})}catch(t){console.warn("[cyborg-hunter-replay] finalize failed:",t);try{this.jsPsych.data.addProperties({replayFinalizeError:String(t&&t.message?t.message:t)})}catch{}}try{this.api.destroy()}catch{}this.api=null}}getLastRecording(){return this._lastRecording}};typeof window<"u"&&(window.jsPsychCyborgHunterReplay=X);})();