cyborg-hunter 0.5.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 (48) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/CITATION.cff +29 -0
  3. package/LICENSE +21 -0
  4. package/README.md +78 -21
  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 +68 -7
  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-cyborg-hunter.js +9 -2
  32. package/src/jspsych/extension-guard-friction.js +32 -12
  33. package/src/jspsych/extension-guard-honeypot.js +25 -1
  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/extension-cyborg-hunter.js +0 -1
  46. package/dist/extension-guard-friction.js +0 -36
  47. package/dist/extension-guard-honeypot.js +0 -1
  48. 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
@@ -4,25 +4,29 @@ 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
+ ### 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 |
9
+ The bundled three-participant synthetic dataset (`examples/synthetic-pilot/` — every number hand-authored, no real participant behind any of it) triages like this:
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
+ | 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 |
17
16
 
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)
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:
18
+
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)
20
+
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).
19
22
 
20
23
  ## Repo layout
21
24
 
22
25
  - `src/core/` — signal-collection library (the monitor)
23
26
  - `src/jspsych/` — jsPsych extension adapters (one per concern)
24
27
  - `src/cli/` + `bin/` — CLI that turns saved data into the triage report
25
- - `tests/`, `docs/`, `studies/` — tests, package docs, full-study fixtures
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))
26
30
 
27
31
  ## Install
28
32
 
@@ -41,9 +45,11 @@ Browser (experiment page) — load via unpkg or copy `dist/*.js` into your proje
41
45
  <!-- Optional: deterrence + bait detection -->
42
46
  <script src="https://unpkg.com/cyborg-hunter/dist/extension-guard-friction.js"></script>
43
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>
44
50
  ```
45
51
 
46
- For production studies, pin a version: `https://unpkg.com/cyborg-hunter@0.4.0/dist/...`.
52
+ For production studies, pin a version: `https://unpkg.com/cyborg-hunter@0.7.0/dist/...`.
47
53
 
48
54
  ## Plug into a jsPsych experiment
49
55
 
@@ -83,7 +89,47 @@ cyborg-hunter report # writes ./cyborg-hunter-report/
83
89
  open cyborg-hunter-report/index.html
84
90
  ```
85
91
 
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).
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
+ ```
120
+
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.
87
133
 
88
134
  ## What it detects
89
135
 
@@ -95,16 +141,18 @@ Output: `summary.csv` (per-participant columns), `triage.md` (ranked list), `eve
95
141
  | Tab-away | `visibilitychange` + `blur`/`focus` | Soft (weighted) |
96
142
  | Browser sidebar | `innerWidth` delta + layout compression | Soft (weighted) |
97
143
  | Suspicious typing speed | chars/sec exceeding preset threshold | Soft (weighted) |
98
- | Synthetic insertion | text appearing without keystrokes | Soft (weighted) |
144
+ | Synthetic insertion | text appearing without keystrokes | Diagnostic (collected, not scored) |
99
145
  | 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) |
146
+ | Idle gaps | input inactivity | Diagnostic (collected, not scored) |
147
+ | AI-extension content scripts | DOM scan for known extension selectors | Diagnostic (collected, not scored) |
102
148
  | Mouse trajectories | 20Hz polling + path-efficiency metrics | Diagnostic |
103
149
  | Window/screen geometry | polled + resize-event capture, with zoom inference | Diagnostic |
104
150
 
105
151
  Three presets: `permissive` / `standard` (default) / `strict`. Per-signal thresholds: [docs/signals-reference.md](docs/signals-reference.md).
106
152
 
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.
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.)
154
+
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).
108
156
 
109
157
  ## What it doesn't detect
110
158
 
@@ -114,21 +162,30 @@ The optional **guard** extensions add: fullscreen / sidebar / focus enforcement
114
162
 
115
163
  ## Documentation
116
164
 
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
117
170
  - [docs/using-cyborg-hunter.md](docs/using-cyborg-hunter.md) — full integration guide
118
171
  - [docs/signals-reference.md](docs/signals-reference.md) — every signal with thresholds per preset
119
172
  - [docs/configuration.md](docs/configuration.md) — config file fields and CLI flags
120
173
  - [docs/cli-reference.md](docs/cli-reference.md) — commands and output structure
121
- - [docs/design-decisions.md](docs/design-decisions.md) — architecture rationale
174
+
175
+ ## Development
176
+
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.
122
178
 
123
179
  ## Citation
124
180
 
125
181
  ```bibtex
126
- @software{konuk2026cyborghunter,
127
- author = {Konuk, Can},
128
- 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},
129
185
  year = {2026},
186
+ version = {0.7.0},
130
187
  url = {https://github.com/konukcan/cyborg-hunter},
131
- version = {0.4.0}
188
+ license = {MIT}
132
189
  }
133
190
  ```
134
191
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyborg-hunter",
3
- "version": "0.5.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
+ }