cyborg-hunter 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/CITATION.cff +2 -2
- package/README.md +3 -3
- package/dist/cyborg-hunter-replay.js +2 -2
- package/dist/cyborg-hunter.esm.js +3 -1
- package/dist/cyborg-hunter.min.js +2 -2
- package/dist/extension-cyborg-hunter.js +1 -1
- package/package.json +1 -1
- package/src/cli/analyzers/score-weights.js +145 -0
- package/src/cli/analyzers/triage.js +55 -39
- package/src/cli/config.js +15 -0
- package/src/cli/renderers/html-index-core.js +22 -10
- package/src/cli/renderers/score-weights.js +16 -0
- package/src/cli/renderers/summary-csv.js +2 -0
- package/src/cli/renderers/trajectories-core.js +2 -1
- package/src/cli/renderers/triage-md.js +9 -2
- package/src/cli/report.js +10 -3
- package/src/jspsych/extension-cyborg-hunter-replay.js +1 -1
- package/src/jspsych/extension-cyborg-hunter.js +1 -1
- package/src/shared/constants.js +1 -1
- package/src/shared/schema.js +1 -0
- package/src/shared/validation.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,29 @@
|
|
|
3
3
|
All notable changes to **cyborg-hunter** are documented here. This project follows
|
|
4
4
|
[Semantic Versioning](https://semver.org).
|
|
5
5
|
|
|
6
|
+
## [0.9.0] — 2026-09-29
|
|
7
|
+
|
|
8
|
+
The report's triage-score weights are configurable from the CLI config. Default
|
|
9
|
+
output is unchanged; the browser library is unchanged apart from recognising
|
|
10
|
+
`scoreWeights` as a known config key.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Report-score weights are configurable: `scoreWeights` in
|
|
14
|
+
`cyborg-hunter.config.json` reweights the triage score's terms, gives a
|
|
15
|
+
weight to any of 16 signals (e.g. `{"synthetic": 1}` for one point per
|
|
16
|
+
synthetic insertion), and can cap a signal's count per participant with
|
|
17
|
+
`{"weight": n, "max": m}`. Entries merge per key onto the defaults, which
|
|
18
|
+
reproduce the 0.8.0 score exactly. The hard/soft/clean tier is unaffected.
|
|
19
|
+
- Every report writes `score-weights.json` with the weights its score used;
|
|
20
|
+
the HTML top bar and `triage.md` state custom weights when they are set.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- The CLI now warns when a config contains the browser library's
|
|
24
|
+
`scoring.soft` / `scoring.hard` rules, which the report never read, and
|
|
25
|
+
points to `scoreWeights`. Invalid or unknown `scoreWeights` entries warn
|
|
26
|
+
(with a "did you mean") and fall back to the defaults.
|
|
27
|
+
- The HTML score breakdown draws the terms the ranking actually used.
|
|
28
|
+
|
|
6
29
|
## [0.8.0] — 2026-09-17
|
|
7
30
|
|
|
8
31
|
The session-replay recorder now writes SessionRecording v2 (`schema_version: 2`), the
|
package/CITATION.cff
CHANGED
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ The bundled three-participant synthetic dataset (`examples/synthetic-pilot/` —
|
|
|
16
16
|
| 2 | SYN-SOFT-02 | soft | 21 | 3 copy events; 1 tab-away ≥10s; 2 tab-aways 3–10s; 1 sidebar event |
|
|
17
17
|
| 3 | SYN-CLEAN-01 | clean | 0 | 1 flicker ≤3s; 1 layout shifts |
|
|
18
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.
|
|
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. By default 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 unless you weight them with `scoreWeights` in the config. See [docs/cli-reference.md → Triage scoring](docs/cli-reference.md#triage-scoring). The HTML report for the same dataset:
|
|
20
20
|
|
|
21
21
|

|
|
22
22
|
|
|
@@ -53,7 +53,7 @@ Browser (experiment page) — load via unpkg or copy `dist/*.js` into your proje
|
|
|
53
53
|
<script src="https://unpkg.com/cyborg-hunter/dist/cyborg-hunter-replay.js"></script>
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
For production studies, pin a version: `https://unpkg.com/cyborg-hunter@0.
|
|
56
|
+
For production studies, pin a version: `https://unpkg.com/cyborg-hunter@0.9.0/dist/...`.
|
|
57
57
|
|
|
58
58
|
## Plug into a jsPsych experiment
|
|
59
59
|
|
|
@@ -204,7 +204,7 @@ Run the test suite with `npm test`. Before publishing, run `scripts/check-public
|
|
|
204
204
|
author = {Konuk, Can and Btesh, Victor and Nunez, Jose Luis},
|
|
205
205
|
title = {cyborg-hunter: detecting AI-tool use in browser-based behavioral experiments},
|
|
206
206
|
year = {2026},
|
|
207
|
-
version = {0.
|
|
207
|
+
version = {0.9.0},
|
|
208
208
|
url = {https://github.com/cyborg-hunter/cyborg-hunter},
|
|
209
209
|
license = {MIT}
|
|
210
210
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
// cyborg-hunter-replay v0.
|
|
1
|
+
// cyborg-hunter-replay v0.9.0 — https://github.com/cyborg-hunter/cyborg-hunter
|
|
2
2
|
(()=>{var Tt=1,De=new WeakSet,Ve=0;function ge(e){return e&&typeof e.has=="function"?e:De}function D(e,r){if(e){var t=ge(r);t===De&&Ve++,t.add(e)}return e}function ze(){return Ve>0}function W(e,r){return!!e&&ge(r).has(e)}function ie(e,r){for(var t=ge(r),n=e;n;){if(t.has(n))return!0;n=n.parentNode}return!1}function Et(e){for(var r=e.attributes||[],t=0;t<r.length;t++)if(r[t].name==="type")return r[t].value==="password";return!1}function Rt(e){return!e||e.tagName!=="INPUT"?!1:e.type==="password"||Et(e)}function q(e,r){if(Rt(e))return!0;if(!r||!e||typeof e.matches!="function")return!1;try{return e.matches(r)}catch{return!1}}function U(e,r){for(var t=e;t;){if(t.nodeType===Tt&&q(t,r))return!0;t=t.parentNode}return!1}var He={participantId:"unknown",tier:"trace",keys:"full",mouseHz:30,redactSelector:"[data-ch-redact]",keepBait:!1,clipboardContent:!1,root:null,keyframeEvery:10,autoSave:{mode:"none"},maxEventsPerTrial:5e4,maxCharsPerTrial:8e6,maxViewportChanges:2e3,maxGuardViolations:40},Ct={created:["session","destroyed"],session:["trial","stopped","destroyed"],trial:["session","stopped","destroyed"],stopped:["destroyed"],destroyed:[]};function je(e){var r=Object.assign({},He,e||{});r.autoSave=Object.assign({},He.autoSave,(e||{}).autoSave||{});var t="created",n=[],a=[],i=[];function u(){var l=i;i=[];for(var p=0;p<l.length;p++)try{l[p]()}catch(w){b.captureFailure("pre_close_flush",w)}}var s=0,d=[],g=new WeakMap,S=new WeakSet;function x(l){try{return JSON.stringify(l).length}catch{return 0}}var m={participantId:r.participantId,tier:r.tier,keys:r.keys,sessionStart:null,sessionStartEpoch:null,userAgent:"",viewport:null,viewportClient:null,observedRoot:null,inheritedRedactionTaint:!1,stylesheets:[],trials:[],guardViolations:[],viewportChanges:[],captureFailures:[],captureStopped:!1,endReason:null},y=null;function R(l){if(Ct[t].indexOf(l)===-1)throw new Error("[cyborg-hunter-replay] invalid lifecycle call: "+t+" \u2192 "+l+". Expected order: attach() \u2192 startSession() \u2192 (startTrial \u2192 endTrial)* \u2192 stopSession() \u2192 destroy()."+(t==="created"?" Did you forget startSession()?":""));t=l}function E(l,p){return s++,{trialIndex:s-1,trialId:l&&l.trialId||(p?"__session__":"trial-"+(s-1)),plugin:l&&l.plugin||"ch:standalone",implicit:!!p,tLoad:p&&s===1?m.sessionStart:performance.now(),tStart:(l&&l.tStart)!=null?l.tStart:null,tDomReady:(l&&l.tDomReady)!=null?l.tDomReady:null,tEnd:null,initialDom:null,initialState:null,events:[]}}function O(){y.tEnd=performance.now(),m.trials.push(y),y=null}function B(l){for(var p=0;p<d.length;p++)try{d[p](l)}catch(w){m.captureFailures.push({channel:"trial_start_hook",message:w&&w.message?String(w.message):String(w),t:performance.now()})}}var A=!1,M=!1;function V(l,p){var w=0;for(var I in p)if(!(I==="t"||!Object.prototype.hasOwnProperty.call(p,I))){if(l[I]!==p[I])return!1;w++}for(var te in l)te==="t"||!Object.prototype.hasOwnProperty.call(l,te)||w--;return w===0}var P=!1;function k(l,p,w){S.add(l),m.captureStopped=!0,!P&&(P=!0,l.events.push({type:"recording.capture_stopped",t:p,reason:"buffer_limit",extensions:{"cyborg-hunter":w}}))}function v(l){if(t==="destroyed")throw new Error("[cyborg-hunter-replay] event pushed to a destroyed recorder");if(!(t==="created"||t==="stopped")&&(y||(y=E(null,!0),B(y)),!S.has(y))){if(y.events.length>=r.maxEventsPerTrial){k(y,l.t,{limit_events:r.maxEventsPerTrial});return}if(r.maxCharsPerTrial!=null){var p=g.get(y)||0;if(p+=x(l),p>r.maxCharsPerTrial){k(y,l.t,{limit_chars:r.maxCharsPerTrial});return}g.set(y,p)}y.events.push(l)}}var b={config:r,startSession:function(){R("session"),m.sessionStart=performance.now(),m.sessionStartEpoch=Date.now(),m.inheritedRedactionTaint=ze();var l=typeof window<"u"?window:null,p=l&&l.visualViewport?l.visualViewport:null;m.viewport=l?{w:l.innerWidth||0,h:l.innerHeight||0,dpr:l.devicePixelRatio||1,scale:p&&typeof p.scale=="number"?p.scale:1,offset_x:p&&p.offsetLeft||0,offset_y:p&&p.offsetTop||0}:null;var w=typeof document<"u"&&document.documentElement?document.documentElement:null;m.viewportClient=w?{w:w.clientWidth||0,h:w.clientHeight||0}:null,m.userAgent=typeof navigator<"u"&&navigator.userAgent?String(navigator.userAgent):"",r.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(l){t==="trial"?(this.captureFailure("lifecycle",new Error("startTrial_without_endTrial: the open trial was auto-closed")),O(),t="session"):y&&O(),R("trial"),y=E(l,!1),B(y)},endTrial:function(){R("session"),y&&O()},stopSession:function(l){u(),t==="trial"&&(t="session"),y&&O(),R("stopped"),m.endReason=l||"finished"},pushRecord:function(l,p){var w=Object.assign({type:l.type,t:null},l);w.t=p??performance.now(),v(w)},pushViewportChange:function(l,p){if(t==="destroyed")throw new Error("[cyborg-hunter-replay] viewport change pushed to a destroyed recorder");if(!(t==="created"||t==="stopped")){var w=m.viewportChanges;if(!(w.length&&V(w[w.length-1],l))){var I=r.maxViewportChanges;if(I!=null&&w.length>=I){A||(A=!0,b.captureFailure("viewport_changes",new Error("viewport_changes cap reached ("+I+"); later viewport geometry is not recorded")));return}w.push(Object.assign({},l,{t:p??performance.now()}))}}},pushGuardViolation:function(l,p){if(t==="destroyed")throw new Error("[cyborg-hunter-replay] guard violation pushed to a destroyed recorder");if(!(t==="created"||t==="stopped")){var w=r.maxGuardViolations;if(w!=null&&m.guardViolations.length>=w){M||(M=!0,b.captureFailure("guard_violations",new Error("guard_violations cap reached ("+w+"); later guard violations are not recorded")));return}m.guardViolations.push(Object.assign({},l,{t:p??performance.now()}))}},noteSnapshotChars:function(l,p){l&&typeof p=="number"&&isFinite(p)&&g.set(l,p)},captureFailure:function(l,p){m.captureFailures.push({channel:l,message:p&&p.message?String(p.message):String(p),t:performance.now()})},onTrialStart:function(l){d.push(l)},setStylesheets:function(l){m.stylesheets=l||[]},setObservedRoot:function(l){m.observedRoot=typeof l=="string"&&l?l:null},addListener:function(l,p,w,I){l.addEventListener(p,w,I||!1),n.push({target:l,event:p,handler:w,options:I||!1})},addInterval:function(l){a.push(l)},addPreCloseFlush:function(l){typeof l=="function"&&i.push(l)},getState:function(){var l=m.trials.slice();return y&&l.push(y),Object.assign({},m,{trials:l,state:t})},destroy:function(){t!=="destroyed"&&(u(),R("destroyed"),n.forEach(function(l){l.options&&l.options._isObserver?l.handler.disconnect():l.target.removeEventListener(l.event,l.handler,l.options)}),n=[],a.forEach(function(l){clearInterval(l)}),a=[])}};return b}var Se=1,we=3,Be=8,It={SCRIPT:!0,NOSCRIPT:!0},kt={VIDEO:!0,AUDIO:!0},Nt="data-record-exclude",Pt={"data-ch-role":!0,"data-ch-decoy":!0},At="ch-decoy",Ot="data-ch-shadow",Mt=/^[a-zA-Z_:][-a-zA-Z0-9_:.]*$/;function Lt(e){return Mt.test(e)&&!/^on/i.test(e)}function oe(e,r){for(var t=e.attributes||[],n=0;n<t.length;n++)if(t[n].name===r)return t[n].value;return null}function F(e,r){if(!e||e.nodeType!==Se)return!1;for(var t=e.attributes||[],n=!1,a=0;a<t.length;a++){var i=t[a].name;if(i===Nt)return!0;Pt[i]&&(n=!0)}return r&&r.keepBait?!1:n||e.id===At}function Ft(e){var r=Ge(e.width,oe(e,"width")),t=Ge(e.height,oe(e,"height"));return r===null||t===null?null:{w:r,h:t}}function Ge(e,r){if(typeof e=="number"&&isFinite(e))return e;var t=parseInt(r,10);return isFinite(t)?t:null}function Dt(e){var r=e.currentSrc||e.src||oe(e,"src");return typeof r=="string"&&r?r:null}function We(e,r,t){return!(!Lt(r)||r==="srcdoc"&&e==="IFRAME"||r==="value"&&t)}function Ue(e,r,t,n){return t==="src"&&r==="IMG"&&typeof e.src=="string"&&e.src?e.src:String(n)}function Ke(e,r,t){for(var n={},a=!1,i=e.attributes||[],u=0;u<i.length;u++){var s=i[u].name;We(r,s,t)&&(n[s]=Ue(e,r,s,i[u].value),s==="src"&&(a=!0))}return!a&&r==="IMG"&&typeof e.src=="string"&&e.src&&(n.src=e.src),e.shadowRoot&&(n[Ot]=""),n}var ue={withheld:!0};function be(e,r,t){var n=e.tagName||"";if(!We(n,r,t))return ue;var a=oe(e,r);return a===null?null:Ue(e,n,r,a)}function _e(e,r){return Ke(e,e.tagName||"",r)}function Q(e){if(!e)return!1;var r=e.nodeType;return r===we||r===Be?!0:r!==Se?!1:!It[e.tagName||""]}function Z(e,r){return!e||e.nodeType!==Se||r?!1:(e.tagName||"")!=="IFRAME"}function $(e,r,t,n){if(!e||!r)return null;t=t||{};for(var a=[],i=e;i&&i!==r;){a.push(i);var u=i.parentNode;!u&&a.length===1&&n&&(u=n),i=u}if(i!==r||!Q(r))return null;for(var s=r,d=a.length-1;d>=0;d--){if(!Z(s,F(s,t))||!Q(a[d]))return s;s=a[d]}return s}function Xe(e,r,t,n,a){if(!Q(e))return null;var i=e.nodeType;if(r.delivery.deliver(e,a||null),!n&&W(e,t.taint)&&(n=!0),i===we||i===Be)return n&&D(e,t.taint),{id:r.registry.idFor(e),kind:i===we?"text":"comment",text:n?"":String(e.textContent||"")};var u=e.tagName||"",s=r.registry.idFor(e);if(F(e,t))return{id:s,kind:"element",tag:u.toLowerCase()};var d=n||q(e,t.redactSelector);d&&D(e,t.taint);var g={id:s,kind:"element",tag:u.toLowerCase(),attrs:Ke(e,u,d),children:[]},S=u==="CANVAS"?Ft(e):null;S&&(g.canvas_size=S);var x=kt[u]&&!d?Dt(e):null;if(x&&(g.media_src=x),!Z(e,!1))return g;for(var m=e.childNodes||[],y=0;y<m.length;y++){var R=Xe(m[y],r,t,d,e);R&&g.children.push(R)}return g}function K(e,r,t){return t=t||{},e?(r.registry.assignTree(e),Xe(e,r,t,U(e,t.redactSelector)||ie(e,t.taint),e.parentNode||null)):null}var Vt=1;function T(e,r,t){return function(n){try{t(n)}catch(a){e.captureFailure(r,a)}}}function j(e){return Math.round(e*10)/10}function N(e){return typeof e=="number"&&isFinite(e)?Math.round(e):0}function Ye(e){return e==null?"":String(e)}var Je="cyborg-hunter";function qe(e,r){var t=e.extensions||(e.extensions={});return t[Je]=Object.assign({},t[Je],r),e}function Qe(e,r){r=r||{};var t=r.doc||document,n=r.win||window,a=r.now||function(){return performance.now()},i=r.raf||(typeof requestAnimationFrame<"u"?requestAnimationFrame.bind(null):function(o){setTimeout(o,16)}),u=e.config,s=r.span||null,d={keepBait:u.keepBait,redactSelector:u.redactSelector,taint:r.taint};function g(o){try{if(!o||typeof o.composedPath!="function")return null;var f=o.composedPath()[0];return f&&f!==o.target?f:null}catch{return null}}function S(o){return x(o,!0).node}function x(o,f){var c={redacted:!1,excluded:!1,node:null,heldEl:null};if(!o)return c;for(var h=!1,_=!1,L=!f||!s,C=o;C&&!(h&&_&&L);){if(!h&&W(C,d.taint)&&(h=!0),!h&&C.nodeType===Vt&&q(C,d.redactSelector)&&(h=!0),!_&&F(C,d)&&(_=!0),!L&&s.delivery.holds(C)){var J=s.registry.peekId(C);c.node=J??null,c.heldEl=C,L=!0}C=C.parentNode}return h&&D(o,d.taint),c.redacted=h,c.excluded=_,c}function m(o){var f=o&&o.target,c=g(o),h=x(f,!0),_=h.redacted,L=h.excluded;if(c){var C=x(c,!1);C.redacted&&(_=!0),C.excluded&&(L=!0)}return{el:f,composed:c,redacted:_,excluded:L,node:h.node,heldEl:h.heldEl}}function y(){var o=t.documentElement;return{cw:o&&o.clientWidth?o.clientWidth:n.innerWidth,ch:o&&o.clientHeight?o.clientHeight:n.innerHeight}}function R(o){return N(o&&o.clientX)}function E(o){return N(o&&o.clientY)}function O(){var o=y(),f=n.visualViewport;return{scroll_x:N(n.scrollX),scroll_y:N(n.scrollY),viewport_w:N(n.innerWidth),viewport_h:N(n.innerHeight),client_w:N(o.cw),client_h:N(o.ch),dpr:typeof n.devicePixelRatio=="number"?n.devicePixelRatio:1,vv_scale:f&&typeof f.scale=="number"?f.scale:1,vv_offset_x:f?j(f.offsetLeft||0):0,vv_offset_y:f?j(f.offsetTop||0):0}}function B(o){var f=o.el;if(!f||!f.tagName||o.excluded&&!o.heldEl)return null;var c=o.excluded&&o.heldEl&&o.heldEl!==f?o.heldEl:f,h={tag:(c.tagName||"").toLowerCase()};!o.redacted&&!o.excluded&&(h.id=c.id||null);try{if(typeof c.getBoundingClientRect=="function"){var _=c.getBoundingClientRect();h.rect={x:j(_.left),y:j(_.top),w:j(_.width),h:j(_.height)}}}catch{}return h.node=o.node,h}function A(o,f){return f.composed&&qe(o,{shadow_retarget:!0}),o}var M=null,V=new Map,P=null,k=null,v=!1,b=!1,l=!1;function p(){v=!1,M&&(e.pushRecord({type:"scroll.window",x:N(n.scrollX),y:N(n.scrollY)},M.t),M=null),V.size>0&&(V.forEach(function(o,f){var c=x(f,!0);if(!c.excluded){var h=c.node;h!=null&&e.pushRecord({type:"scroll.element",node:h,x:N(f.scrollLeft),y:N(f.scrollTop)},o.t)}}),V.clear())}function w(){var o=n.visualViewport;return{w:N(n.innerWidth),h:N(n.innerHeight),dpr:typeof n.devicePixelRatio=="number"?n.devicePixelRatio:1,scale:o&&typeof o.scale=="number"?o.scale:1,offset_x:o?j(o.offsetLeft||0):0,offset_y:o?j(o.offsetTop||0):0}}function I(){P&&(e.pushViewportChange(w(),P.t),P=null)}function te(){k&&(e.pushViewportChange(w(),k.t),k=null)}function ce(){if(b=!1,l=!1,P&&k){var o=Math.max(P.t,k.t);P=null,k=null,e.pushViewportChange(w(),o);return}I(),te()}function H(){p(),ce()}function re(o,f){o.camera=O();var c=B(f);return c&&(o.anchor=c),o}var wt=1e3/(u.mouseHz||30),Ae=-1/0;e.addListener(t,"mousemove",T(e,"mouse",function(o){var f=a();f-Ae<wt||(Ae=f,e.pushRecord({type:"mouse.move",x:R(o),y:E(o)},f))}),{passive:!0,capture:!0}),[["mousedown","mouse.down"],["mouseup","mouse.up"],["click","mouse.click"]].forEach(function(o){e.addListener(t,o[0],T(e,"mouse",function(f){H();var c=m(f),h=re({type:o[1],x:R(f),y:E(f),button:typeof f.button=="number"?f.button:0,target:c.node},c);e.pushRecord(A(h,c),a())}),{passive:!0,capture:!0})}),u.keys!=="off"&&[["keydown","key.down"],["keyup","key.up"]].forEach(function(o){e.addListener(t,o[0],T(e,"keys",function(f){var c=m(f);if(c.redacted||c.excluded){e.pushRecord(A({type:o[1],redacted:!0},c),a());return}var h=o[1]==="key.down"&&!f.repeat;h&&H();var _={type:o[1],key:Ye(f.key),code:Ye(f.code),mods:{ctrl:!!f.ctrlKey,shift:!!f.shiftKey,alt:!!f.altKey,meta:!!f.metaKey},repeat:!!f.repeat,target:c.node};h&&re(_,c),e.pushRecord(A(_,c),a())}),!0)});function Oe(o){var f=null,c=null;try{f=o("text")}catch{}if(u.clipboardContent)try{c=o("text/html")||null}catch{}return{text:typeof f=="string"?f:null,html:c}}function ne(o,f,c){var h={type:o,target:f.node,text:null,html:null,len:c&&c.text!=null?c.text.length:null};return f.redacted&&(h.redacted=!0),f.excluded&&(h.len=null),u.clipboardContent&&!f.redacted&&!f.excluded&&c&&(h.text=c.text,h.html=c.html,h.len=null),A(h,f)}e.addListener(t,"paste",T(e,"clipboard",function(o){H();var f=m(o),c=o.clipboardData,h=Oe(function(_){return c.getData(_)});e.pushRecord(ne("clipboard.paste",f,h),a())}),!0),e.addListener(t,"copy",T(e,"clipboard",function(o){H(),e.pushRecord(ne("clipboard.copy",m(o),null),a())}),!0),e.addListener(t,"cut",T(e,"clipboard",function(o){H(),e.pushRecord(ne("clipboard.cut",m(o),null),a())}),!0),e.addListener(t,"drop",T(e,"clipboard",function(o){H();var f=m(o),c=o.dataTransfer,h=Oe(function(_){return c.getData(_)});e.pushRecord(ne("clipboard.drop",f,h),a())}),!0);var pe=new Map,he=!1;function St(o){return o.value!=null?String(o.value):o.isContentEditable&&o.textContent!=null?String(o.textContent):""}function bt(o){for(var f=[],c=o.options||[],h=0;h<c.length;h++)c[h].selected&&f.push(typeof c[h].value=="string"?c[h].value:String(c[h].textContent||""));return f}function Me(o){if(!o||o.tagName!=="INPUT")return!1;if(String(o.type||"").toLowerCase()==="file")return!0;var f=typeof o.getAttribute=="function"?o.getAttribute("type"):null;return typeof f=="string"&&f.toLowerCase()==="file"}function _t(o,f){var c=x(o,!0);if(c.excluded)return null;var h=f?x(f,!1):null;if(h&&h.excluded)return null;var _=c.node;if(_==null)return null;var L=c.redacted||!!(h&&h.redacted);if(Me(o)||f&&Me(f))return null;var C=String(o.type||"").toLowerCase();if(o.tagName==="SELECT")return L?null:{type:"input.select",node:_,values:bt(o)};if(o.tagName==="INPUT"&&(C==="checkbox"||C==="radio"))return L?null:{type:"input.checked",node:_,checked:!!o.checked};var J=St(o);return L?{type:"input.value",node:_,redacted:!0,value_len:J.length}:{type:"input.value",node:_,value:J}}function xt(){he=!1,pe.forEach(function(o,f){var c=_t(f,o.composed);c&&(o.composed&&qe(c,{shadow_retarget:!0}),e.pushRecord(c,o.t))}),pe.clear()}e.addListener(t,"input",T(e,"input",function(o){o.target&&(pe.set(o.target,{t:a(),composed:g(o)}),he||(he=!0,i(T(e,"input",xt))))}),!0);var ae=new Set;function Le(){ae.forEach(function(o){o&&o.isConnected===!1&&ae.delete(o)})}e.addListener(n,"scroll",T(e,"scroll",function(o){var f=o&&o.target&&o.target.tagName?o.target:null;f?(V.set(f,{t:a()}),ae.add(f)):M={t:a()},v||(v=!0,i(T(e,"scroll",p)))}),{passive:!0,capture:!0});function ve(o){for(var f=[],c=0;c<(o?o.length:0);c++){var h=o[c];f.push({id:typeof h.identifier=="number"?h.identifier:c,x:R(h),y:E(h)})}return f}e.addListener(t,"touchstart",T(e,"touch",function(o){H();var f=m(o),c=re({type:"touch.start",touches:ve(o.touches)},f);e.pushRecord(A(c,f),a())}),{passive:!0,capture:!0});var me=!1,ye=null,Fe=0;return e.addListener(t,"touchmove",T(e,"touch",function(o){ye=ve(o.touches),Fe=a(),!me&&(me=!0,i(T(e,"touch",function(){me=!1,ye&&e.pushRecord({type:"touch.move",touches:ye},Fe)})))}),{passive:!0,capture:!0}),e.addListener(t,"touchend",T(e,"touch",function(o){H();var f=m(o),c=re({type:"touch.end",touches:ve(o.changedTouches)},f);e.pushRecord(A(c,f),a())}),{passive:!0,capture:!0}),e.addListener(n,"blur",T(e,"focus",function(){e.pushRecord({type:"blur"},a())})),e.addListener(n,"focus",T(e,"focus",function(){e.pushRecord({type:"focus"},a())})),e.addListener(t,"visibilitychange",T(e,"focus",function(){e.pushRecord({type:t.hidden?"visibility.hidden":"visibility.visible"},a())})),e.addListener(n,"resize",T(e,"viewport",function(){P={t:a()},b||(b=!0,i(T(e,"viewport",ce)))})),n.visualViewport&&typeof n.visualViewport.addEventListener=="function"&&["resize","scroll"].forEach(function(o){e.addListener(n.visualViewport,o,T(e,"viewport",function(){k={t:a()},l||(l=!0,i(T(e,"viewport",ce)))}))}),e.onTrialStart(function(){Le()}),{getScrolledElements:function(){return Le(),ae}}}var xe=1,Ze={childList:!0,attributes:!0,attributeOldValue:!0,characterData:!0,subtree:!0};function $e(e,r){var t=[];if(!e||!e.length||!r||!r.root||!r.span)return t;var n={out:t,root:r.root,span:r.span,registry:r.span.registry,delivery:r.span.delivery,t:r.t,opts:r,pending:new Map,pendingAdd:new Map,attrs:new Map,attrOrder:[],transitioned:new Set,restored:new Set,payloads:new Set,noHeldFrom:new Map};zt(e,n),jt(n);for(var a=0;a<e.length;a++){var i=e[a];i&&(i.type==="childList"?Gt(i,n):i.type==="attributes"?Xt(i,n):i.type==="characterData"&&Qt(i,n))}return t}function zt(e,r){for(var t=0;t<e.length;t++){var n=e[t];if(n){if(n.type==="childList"){for(var a=n.removedNodes||[],i=0;i<a.length;i++)r.pending.set(a[i],(r.pending.get(a[i])||0)+1);for(var u=n.addedNodes||[],s=0;s<u.length;s++)r.pendingAdd.set(u[s],(r.pendingAdd.get(u[s])||0)+1)}else if(n.type==="attributes"&&n.target&&n.attributeName){var d=r.attrs.get(n.target);d||(d=new Map,r.attrs.set(n.target,d),r.attrOrder.push(n.target));var g=it(n.target,n)||n.attributeName,S=d.get(g);S?S.count++:d.set(g,{first:n.oldValue,count:1})}}}}function se(e,r,t){var n=t.attrs.get(e),a=n&&n.get(r),i=et(e,r);return a?a.count===1&&i===null?a.first==null?"":a.first:a.first==null?null:a.first:i}function et(e,r){for(var t=e.attributes||[],n=0;n<t.length;n++)if(t[n].name===r)return t[n].value;return null}function Ht(e,r){var t=se(e,"id",r);return F({nodeType:xe,id:t??"",attributes:tt(e,r)},r.opts)}function tt(e,r){for(var t=[],n={},a=e.attributes||[],i=0;i<a.length;i++){var u=a[i].name;n[u]=!0;var s=se(e,u,r);s!==null&&t.push({name:u,value:s})}var d=r.attrs.get(e);return d&&d.forEach(function(g,S){if(!n[S]){var x=se(e,S,r);x!==null&&t.push({name:S,value:x})}}),t}function jt(e){for(var r=new Map,t=0;t<e.attrOrder.length;t++){var n=e.attrOrder[t];if(!(!n||n.nodeType!==xe)&&e.delivery.holds(n)){var a=e.registry.peekId(n);a!=null&&Ht(n,e)!==F(n,e.opts)&&r.set(n,a)}}var i=new Set;r.forEach(function(u,s){rt(s,r,i,e)})}function rt(e,r,t,n){if(!t.has(e)){t.add(e);for(var a=[],i=e.parentNode;i&&(r.has(i)&&a.push(i),i!==n.root);i=i.parentNode);for(var u=a.length-1;u>=0;u--)rt(a[u],r,t,n);n.transitioned.add(e),n.delivery.holds(e)&&(Yt(e,n)||(F(e,n.opts)?Jt(e,r.get(e),n):qt(e,r.get(e),n)))}}function Gt(e,r){for(var t=e.removedNodes||[],n=0;n<t.length;n++)Bt(t[n],e.target,r);for(var a=e.addedNodes||[],i=0;i<a.length;i++)Wt(a[i],e.target,r)}function Bt(e,r,t){if(at(t.pending,e),!nt(e,t)){var n=t.delivery;if(!(!n.holds(e)||n.parentOf(e)!==r)&&$(r,t.root,t.opts)===r){var a=t.registry.peekId(e);a!=null&&(t.out.push({type:"dom.remove",t:t.t,node:a}),n.purge(e))}}}function nt(e,r){for(var t=e&&e.parentNode;t;t=t.parentNode){if(r.payloads.has(t))return!0;if(t===r.root)break}return!1}function at(e,r){var t=(e.get(r)||0)-1;t>0?e.set(r,t):e.delete(r)}function Wt(e,r,t){at(t.pendingAdd,e);var n=t.delivery,a=e.parentNode;if(r===a&&!nt(e,t)&&!(n.holds(e)&&n.parentOf(e)===a)&&$(e,t.root,t.opts)===e&&n.holds(a)){var i=Ut(e,t);if(Te(e,t),!!n.holds(a)){var u=t.registry.peekId(a);if(u!=null){var s=K(e,t.span,t.opts);s&&(t.out.push({type:"dom.add",t:t.t,parent:u,before:i,node:s}),t.payloads.add(e),Kt(e,t))}}}}function Te(e,r){var t=r.delivery;if(t.holds(e)&&t.parentOf(e)!==e.parentNode){var n=r.registry.peekId(e);n!=null&&r.out.push({type:"dom.remove",t:r.t,node:n}),t.purge(e)}for(var a=e.childNodes||[],i=0;i<a.length;i++)Te(a[i],r)}function Ut(e,r){var t=e.nextSibling;if(!t)return null;var n=r.noHeldFrom.get(e.parentNode);if(n!==void 0&&(n===e||n===t))return null;for(var a=t;a;a=a.nextSibling)if(!(r.pending.get(a)>0)&&!(r.pendingAdd.get(a)>0)&&r.delivery.holds(a)&&r.delivery.parentOf(a)===e.parentNode){var i=r.registry.peekId(a);if(i!=null)return i}return r.noHeldFrom.set(e.parentNode,t),null}function Kt(e,r){var t=e.parentNode,n=r.noHeldFrom.get(t);if(n!==void 0){if(n===e){e.nextSibling?r.noHeldFrom.set(t,e.nextSibling):r.noHeldFrom.delete(t);return}e.nextSibling!==n&&r.noHeldFrom.delete(t)}}function Xt(e,r){var t=e.target,n=e.attributeName;if(!(!t||t.nodeType!==xe||!n)&&!r.transitioned.has(t)&&r.delivery.holds(t)&&$(t,r.root,r.opts)===t){var a=r.registry.peekId(t);if(a!=null&&!F(t,r.opts)){var i=it(t,e);if(i!==null){var u=le(t,r),s=be(t,i,u);if(s===ue){u&&D(t,r.opts.taint);return}se(t,i,r)!==et(t,i)&&r.out.push({type:"dom.attr",t:r.t,node:a,name:i,value:s})}}}}function it(e,r){var t=r.attributeNamespace;if(!t)return r.attributeName;for(var n=e.attributes||[],a=0;a<n.length;a++)if(n[a].localName===r.attributeName&&n[a].namespaceURI===t)return n[a].name;return null}function le(e,r){return U(e,r.opts.redactSelector)||ie(e,r.opts.taint)}function Yt(e,r){for(var t=e.parentNode;t;t=t.parentNode){if(r.restored.has(t))return!0;if(t===r.root)break}return!1}function Jt(e,r,t){if(Z(e,!1))for(var n=t.delivery.childrenOf(e),a=0;a<n.length;a++){var i=t.registry.peekId(n[a]);i!=null&&(t.out.push({type:"dom.remove",t:t.t,node:i}),t.delivery.purge(n[a]))}for(var u=le(e,t),s=_e(e,u),d=tt(e,t),a=0;a<d.length;a++)be(e,d[a].name,u)!==ue&&(s[d[a].name]=null);for(var g in s)Object.prototype.hasOwnProperty.call(s,g)&&t.out.push({type:"dom.attr",t:t.t,node:r,name:g,value:null})}function qt(e,r,t){var n=_e(e,le(e,t));for(var a in n)Object.prototype.hasOwnProperty.call(n,a)&&t.out.push({type:"dom.attr",t:t.t,node:r,name:a,value:n[a]});if(t.restored.add(e),t.payloads.add(e),!!Z(e,!1)){for(var i=e.childNodes||[],u=0;u<i.length;u++){var s=i[u];if(Q(s)&&!(t.delivery.holds(s)&&t.delivery.parentOf(s)===e)){Te(s,t);var d=K(s,t.span,t.opts);d&&(t.out.push({type:"dom.add",t:t.t,parent:r,before:null,node:d}),t.payloads.add(s))}}t.noHeldFrom.delete(e)}}function Qt(e,r){var t=e.target;if(r.delivery.holds(t)&&$(t,r.root,r.opts)===t){var n=r.registry.peekId(t);if(n!=null){if(le(t,r)){D(t,r.opts.taint);return}r.out.push({type:"dom.text",t:r.t,node:n,text:String(t.textContent==null?"":t.textContent)})}}}var Zt="video, audio",$t="input, textarea, select",er={password:!0,file:!0};function fe(e){return Math.round(e)}function tr(e){return Math.round(e*1e3)/1e3}function ot(e,r){if(typeof e.getAttribute=="function")return e.getAttribute(r);for(var t=e.attributes||[],n=0;n<t.length;n++)if(t[n].name===r)return t[n].value;return null}function rr(e,r){return ot(e,r)!==null}function ut(e,r){var t=[];if(typeof e.matches=="function")try{e.matches(r)&&t.push(e)}catch{}if(typeof e.querySelectorAll!="function")return t;for(var n=e.querySelectorAll(r),a=0;a<n.length;a++)t.push(n[a]);return t}function Re(e,r,t){return!r.delivery.holds(e)||F(e,t)?null:r.registry.peekId(e)}function Ce(e,r){return U(e,r.redactSelector)||W(e,r.taint)?(D(e,r.taint),!0):!1}function nr(e,r,t){var n=[],a=t.scrolled;return!a||typeof a.forEach!="function"||a.forEach(function(i){if(i&&!Ce(i,t)){var u=Re(i,r,t);if(u!==null){var s=fe(i.scrollLeft||0),d=fe(i.scrollTop||0);s===0&&d===0||n.push({node:u,x:s,y:d})}}}),n}function ar(e,r,t){for(var n=[],a=ut(e,Zt),i=0;i<a.length;i++){var u=a[i];if(!Ce(u,t)){var s=Re(u,r,t);if(s!==null){var d=typeof u.currentTime=="number"&&isFinite(u.currentTime)?tr(u.currentTime):0,g=u.paused!==!1;d===0&&g||n.push({node:s,current_time:d,paused:g})}}}return n}function ir(e){return typeof e.defaultValue=="string"?e.defaultValue:""}function Ee(e){return typeof e.value=="string"?e.value:String(e.textContent||"")}function or(e){if(typeof e.size=="number"&&e.size>0)return e.size;var r=parseInt(ot(e,"size"),10);return r>0?r:1}function ur(e){for(var r=e.options?Array.prototype.slice.call(e.options):[],t=[],n=[],a=null,i=0;i<r.length;i++){var u=r[i];u.selected&&t.push(Ee(u)),rr(u,"selected")&&n.push(Ee(u)),a===null&&!u.disabled&&(a=u)}return!e.multiple&&or(e)===1&&n.length===0&&a&&n.push(Ee(a)),t.length===n.length&&t.every(function(s,d){return s===n[d]})?null:{selected:t}}function sr(e){var r=e.tagName;if(r==="SELECT")return ur(e);if(r==="INPUT"){var t=String(e.type||"text").toLowerCase();if(er[t])return null;if(t==="checkbox"||t==="radio")return!!e.checked==!!e.defaultChecked?null:{checked:!!e.checked}}var n=typeof e.value=="string"?e.value:String(e.value==null?"":e.value);return n===ir(e)?null:{value:n}}function lr(e,r,t){for(var n=[],a=ut(e,$t),i=0;i<a.length;i++){var u=a[i];if(!Ce(u,t)){var s=Re(u,r,t);if(s!==null){var d=sr(u);d&&n.push(Object.assign({node:s},d))}}}return n}function st(e,r,t){if(t=t||{},!e||!r||r.registry.count===0)return null;var n=t.win!==void 0?t.win:typeof window<"u"?window:null,a={x:n?fe(n.scrollX||0):0,y:n?fe(n.scrollY||0):0},i={scroll:a,element_scroll:nr(e,r,t),media:ar(e,r,t),form:lr(e,r,t)};return a.x===0&&a.y===0&&!i.element_scroll.length&&!i.media.length&&!i.form.length?null:i}function lt(){let e=new WeakMap,r=1;function t(u){let s=e.get(u);return s!==void 0?s:(e.set(u,r),r++)}function n(u){let s=t(u),d=u.childNodes;if(d)for(let g=0;g<d.length;g++)n(d[g]);return s}function a(u){let s=e.get(u);return s===void 0?null:s}function i(){e=new WeakMap,r=1}return{idFor:t,assignTree:n,peekId:a,resetSpan:i,get count(){return r-1}}}function ft(){var e=new WeakMap,r=new WeakMap;function t(a){var i=e.get(a);if(i){var u=r.get(i);u&&u.delete(a)}}function n(a){var i=r.get(a);i&&(i.forEach(function(u){n(u)}),r.delete(a)),e.delete(a)}return{deliver:function(a,i){if(e.has(a)&&t(a),e.set(a,i||null),i){var u=r.get(i);u||(u=new Set,r.set(i,u)),u.add(a)}},purge:function(a){t(a),n(a)},holds:function(a){return e.has(a)},parentOf:function(a){var i=e.get(a);return i===void 0?null:i},childrenOf:function(a){var i=r.get(a);return i?Array.from(i):[]},reset:function(){e=new WeakMap,r=new WeakMap}}}function ee(){var e=lt(),r=ft();return{registry:e,delivery:r,reset:function(){e.resetSpan(),r.reset()}}}function fr(e){for(var r=[],t=e&&e.styleSheets||[],n=0;n<t.length;n++){var a=t[n],i=a.href||null,u=a.media&&a.media.mediaText?a.media.mediaText:null,s=null;try{for(var d=a.cssRules,g=[],S=0;S<d.length;S++)g.push(d[S].cssText);s=g.join(`
|
|
3
|
-
`)}catch{s=null}r.push(i?{id:n+1,kind:"link",href:i,css:s,media:u}:{id:n+1,kind:"inline",css:s??"",media:u})}return r}function dr(e,r){if(typeof r!="function"||!Array.isArray(e))return Promise.resolve();for(var t=[],n=0;n<e.length;n++){var a=e[n];!a||a.kind!=="link"||a.css!=null||typeof a.href!="string"||!/^https?:/i.test(a.href)||t.push(cr(a,r))}return Promise.all(t).then(function(){})}function cr(e,r){return Promise.resolve().then(function(){return r(e.href,{mode:"cors",credentials:"omit"})}).then(function(t){return!t||!t.ok?null:t.text()}).then(function(t){typeof t=="string"&&e.css==null&&(e.css=t)}).catch(function(){})}function de(e){if(e==null)return 0;try{return JSON.stringify(e).length}catch{return 0}}function pr(e,r){return!e.hasKeyframe||typeof r=="number"&&r>=1&&e.segments>=r?!0:e.lastSnapshotChars>0&&e.patchChars>=e.lastSnapshotChars}function dt(e,r){r=r||{};var t=r.doc||document,n=r.win||window,a=r.now||function(){return performance.now()},i=r.MutationObserver||(typeof MutationObserver<"u"?MutationObserver:null),u=r.span||ee(),s=typeof r.scrolled=="function"?r.scrolled:function(){return null},d={keepBait:e.config.keepBait,redactSelector:e.config.redactSelector,taint:r.taint},g=!1,S=x();function x(){var v=e.config.root;if(typeof v=="string"&&v){var b=null;try{b=t.querySelector(v)}catch{b=null}return b?(g=!0,b):(e.captureFailure("observed_root",new Error('root selector "'+v+'" matched nothing at startSession(); observing document.body instead')),t.body)}return v||t.body}function m(){return g?e.config.root:S&&S.id?"#"+S.id:null}e.setObservedRoot(m());try{var y=fr(t);e.setStylesheets(y);var R=t&&t.defaultView;R&&typeof R.fetch=="function"&&dr(y,R.fetch.bind(R))}catch(v){e.captureFailure("stylesheets",v)}var E={hasKeyframe:!1,segments:0,patchChars:0,lastSnapshotChars:0},O=e.config.keyframeEvery;O!=null&&typeof O!="number"&&console.warn("[cyborg-hunter-replay] keyframeEvery must be a number; got "+typeof O+" ("+JSON.stringify(O)+"). The segment fallback is DISABLED for this recording \u2014 keyframes will be taken on accumulated patch size alone."),e.onTrialStart(function(v){if(v.implicit&&E.hasKeyframe){E.segments++;return}if(!pr(E,e.config.keyframeEvery)){E.segments++;return}try{u.reset();var b=K(S,u,d),l=st(S,u,{win:n,scrolled:s(),keepBait:d.keepBait,redactSelector:d.redactSelector,taint:d.taint}),p=de(b)+de(l),w=e.config.maxCharsPerTrial;w!=null&&p>w?(e.captureFailure("dom_snapshot",new Error("initial DOM keyframe of "+p+" chars exceeds maxCharsPerTrial "+w+"; dropped to bound payload size")),v.initialDom=null,v.initialState=null,u.reset(),B()):(v.initialDom=b,v.initialState=l,e.noteSnapshotChars(v,p),E.hasKeyframe=!0,E.segments=1,E.patchChars=0,E.lastSnapshotChars=p)}catch(I){e.captureFailure("dom_snapshot",I),v.initialDom=null,v.initialState=null,u.reset(),B()}});function B(){E.hasKeyframe=!1,E.segments=0,E.patchChars=0,E.lastSnapshotChars=0}if(i){var A=function(v){try{for(var b=$e(v,{root:S,span:u,t:a(),keepBait:d.keepBait,redactSelector:d.redactSelector,taint:d.taint}),l=0;l<b.length;l++)e.pushRecord(b[l],b[l].t);b.length&&(E.patchChars+=de(b))}catch(p){e.captureFailure("mutations",p)}},M=new i(A);try{M.observe(S,Ze),e.addListener({addEventListener:function(){},removeEventListener:function(){}},"_mutation_observer",M,{_isObserver:!0}),e.addPreCloseFlush(function(){var v=M.takeRecords();v&&v.length&&A(v)})}catch(v){e.captureFailure("mutations",v)}}function V(){var v=K(S,ee(),d),b=e.config.maxCharsPerTrial;return b!=null&&de(v)>b?(e.captureFailure("guard_violation",new Error("pre-scramble DOM snapshot exceeds maxCharsPerTrial "+b+"; withheld")),null):v}if(n.GuardFriction&&typeof n.GuardFriction.onViolation=="function")try{var P=e.getState().state;if((P==="session"||P==="trial")&&typeof n.GuardFriction.getCurrentState=="function"){var k=n.GuardFriction.getCurrentState();k&&k.in_violation&&e.pushGuardViolation({reason:k.current_reason||"unknown",phase:"start",synthesized_at_subscribe:!0,dom_at_subscribe:V()},a())}n.GuardFriction.onViolation(function(v){try{v&&v.phase==="start"?e.pushGuardViolation({reason:v.reason,phase:"start",pre_scramble_dom:V()},a()):v&&v.phase&&e.pushGuardViolation({reason:v.reason,phase:v.phase,duration_ms:v.duration!=null?v.duration:null},a())}catch(b){e.captureFailure("guard_violation",b)}})}catch(v){e.captureFailure("guard_violation",v)}}var Ie="0.8.0";function ct(e){return String(e).replace(/[^a-zA-Z0-9_.-]/g,"_")}var ke=2;function G(e,r){return e==null?null:Math.round((e-r)*10)/10}function z(e,r){return(e||[]).map(function(t){var n=Object.assign({},t);return typeof n.t=="number"&&(n.t=G(n.t,r)),typeof n.start=="number"&&(n.start=G(n.start,r)),n})}function pt(e){return e.slice().sort(function(r,t){return r.t-t.t})}var hr={w:0,h:0,dpr:1,scale:1,offset_x:0,offset_y:0};function vr(e){return e?{w:X(e.w),h:X(e.h),dpr:X(e.dpr,1),scale:X(e.scale,1),offset_x:X(e.offset_x),offset_y:X(e.offset_y)}:Object.assign({},hr)}function X(e,r){return typeof e=="number"&&isFinite(e)?e:r??0}function ht(e,r){r=r||{};var t=e.sessionStart;if(t==null)throw new Error("[cyborg-hunter-replay] cannot serialize before startSession() \u2014 call startSession() first, or the timestamps would be meaningless (Unix epoch).");var n=r.chSessionReport||null,a=t,i=e.trials.map(function(s,d){var g=pt(s.events),S=g.length>0?g[g.length-1].t:s.tLoad;return S>a&&(a=S),s.tEnd!=null&&s.tEnd>a&&(a=s.tEnd),{index:d,label:s.trialId!=null?s.trialId:null,plugin:s.plugin!=null?s.plugin:null,t_start:G(s.tStart,t),t_dom_ready:G(s.tDomReady,t),t_load:G(s.tLoad,t),t_end:G(s.tEnd!=null?s.tEnd:S,t),initial_dom:s.initialDom||null,initial_state:s.initialState||null,events:g.map(function(x){var m=Object.assign({},x);return m.t=G(x.t,t),m}),host_data:s.hostData!=null?s.hostData:null,extensions:s.implicit?{"cyborg-hunter":{implicit:!0}}:null}}),u={replay_version:Ie,tier:e.tier,keys:e.keys,viewport_client:e.viewportClient||null,ch_version:n&&n.libraryVersion||null,preset:n&&n.config?n.config.preset:null,scoring:n?{hard_score:n.hardScore||{},soft_score:n.softScore!=null?n.softScore:null,soft_score_threshold:n.softScoreThreshold!=null?n.softScoreThreshold:null,any_hard_triggered:!!n.anyHardTriggered,trials_completed:n.trialsCompleted!=null?n.trialsCompleted:null}:null,session_signals:n?{sidebar_events:z(n.sidebarEvents,t),ai_extensions_found:z(n.aiExtensionsFound,t),devtools_events:z(n.devToolsEvents,t),window_positions:z(n.windowPositions,t),layout_shifts:z(n.layoutShifts,t),zoom_changes:z(n.zoomChanges,t),extension_injections:z(n.extensionInjections,t)}:null,guard_violations:z(e.guardViolations,t),capture_failures:z(e.captureFailures,t),capture_stopped:!!e.captureStopped,inherited_redaction_taint:!!e.inheritedRedactionTaint};return{schema_version:ke,recorder:{name:"cyborg-hunter-replay",version:Ie},host:r.host||null,participant_id:e.participantId!=null?e.participantId:null,recording_started_at:new Date(e.sessionStartEpoch).toISOString(),recording_started_at_perf:t,user_agent:e.userAgent||"",viewport:vr(e.viewport),observed_root:e.observedRoot||null,stylesheets:e.stylesheets||[],stylesheet_events:[],viewport_changes:pt(e.viewportChanges||[]).map(function(s){var d=Object.assign({},s);return d.t=G(s.t,t),d}),rng:null,rng_calls:null,ended_at_perf:a,end_reason:e.endReason||"aborted",truncated:!!e.captureStopped,extensions:{"cyborg-hunter":u},segments:i}}var mr="https://pipe.jspsych.org/api/data/";function yr(e){var r=e&&e.extensions;return r&&r["cyborg-hunter"]||{}}function vt(e){var r=ct(e.participant_id),t=Date.parse(e.recording_started_at);return r+"-replay-"+t+".json"}function mt(e,r){var t=yr(e);return{schema_version:e.schema_version,tier:t.tier!=null?t.tier:null,saved_to:r,bytes_uncompressed:JSON.stringify(e).length,capture_failures:(t.capture_failures||[]).map(function(n){return n.channel}),capture_stopped:!!e.truncated}}async function yt(e,r){var t=r||{mode:"none"},n=vt(e),a=typeof window<"u"?window:{};if(t.mode==="none")return{saved_to:"none",filename:n};if(t.mode==="datapipe"){if(!t.experimentId)return{saved_to:"failed",filename:n,error:'autoSave.mode "datapipe" requires autoSave.experimentId'};try{var i=JSON.stringify(e);if(a.jsPsychPipe&&typeof a.jsPsychPipe.saveData=="function")await a.jsPsychPipe.saveData(t.experimentId,n,i);else{var u=a.fetch||(typeof fetch<"u"?fetch:null);if(!u)throw new Error("no fetch available for DataPipe POST");var s=await u(mr,{method:"POST",headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify({experimentID:t.experimentId,filename:n,data:i})});if(s&&s.ok===!1)throw new Error("DataPipe HTTP error")}return{saved_to:"datapipe:"+t.experimentId+"/"+n,filename:n}}catch(y){return console.error("[cyborg-hunter-replay] DataPipe autosave failed:",y),{saved_to:"failed",filename:n,error:y&&y.message?y.message:String(y)}}}if(t.mode==="download")try{var d=typeof document<"u"?document:null,g=a.Blob||(typeof Blob<"u"?Blob:null),S=a.URL||(typeof URL<"u"?URL:null);if(!d||!g||!S)throw new Error("download mode needs a browser");var x=new g([JSON.stringify(e)],{type:"application/json"}),m=d.createElement("a");return m.href=S.createObjectURL(x),m.download=n,d.body.appendChild(m),m.click(),d.body.removeChild(m),S.revokeObjectURL(m.href),{saved_to:"download",filename:n}}catch(y){return console.error("[cyborg-hunter-replay] download autosave failed:",y),{saved_to:"failed",filename:n,error:y&&y.message?y.message:String(y)}}return{saved_to:"failed",filename:n,error:'unknown autoSave.mode "'+t.mode+'"'}}async function gt(e){var r=JSON.stringify(e),t=typeof window<"u"?window:{},n=t.CompressionStream||(typeof CompressionStream<"u"?CompressionStream:null),a=t.Blob||(typeof Blob<"u"?Blob:null);if(!a)return null;if(!n)return new a([r],{type:"application/json"});var i=new a([r]).stream().pipeThrough(new n("gzip")),u=t.Response||(typeof Response<"u"?Response:null),s=await new u(i).arrayBuffer();return new a([s],{type:"application/gzip"})}var Y=null;function Ne(e){Y&&(console.warn("[cyborg-hunter-replay] attach() called while a recorder is active \u2014 replacing the previous instance (its unsaved recording is discarded)."),Y.destroy(),Y=null);var r=je(e),t={_recorder:r,config:r.config,startSession:function(){r.startSession();var n=ee(),a=Qe(r,{span:n});return(r.config.tier==="dom"||r.config.tier==="canvas")&&dt(r,{span:n,scrolled:a.getScrolledElements}),t},startTrial:function(n){return r.startTrial(n),t},endTrial:function(){return r.endTrial(),t},stopSession:function(n){return r.stopSession(n),t},getRecording:function(n){return ht(r.getState(),n||{})},getRecordingCompressed:function(n){return gt(t.getRecording(n))},autoSaveNow:async function(n){if(r.getState().sessionStart==null)return{recording:null,saveResult:{saved_to:"no_session"},meta:{schema_version:ke,saved_to:"no_session",note:"recorder was never started; nothing to save"}};var a=t.getRecording(n),i=await yt(a,r.config.autoSave),u=mt(a,i.saved_to);return i.error&&(u.save_error=i.error),{recording:a,saveResult:i,meta:u}},destroy:function(){r.destroy(),Y===t&&(Y=null)}};return Y=t,t}typeof window<"u"&&(window.CyborgHunterReplay={attach:Ne});var Pe=class{static info={name:"cyborg-hunter-replay",version:"0.8.0",data:{}};constructor(r){this.jsPsych=r,this.api=null,this._chMonitor=null,this._lastRecording=null,this._monitoring=!1}initialize(r){this.params=r||{},this.params.keyframeEvery!=null&&this.params.keyframeEvery!==1&&console.warn("[cyborg-hunter-replay] ignoring keyframeEvery: "+this.params.keyframeEvery+" \u2014 jsPsych wipes the display between trials, so every trial segment is recorded as a keyframe."),this.api=Ne(Object.assign({},this.params,{keyframeEvery:1})),this.api.startSession(),this._stashChMonitor()}_stashChMonitor(){try{var r=this.jsPsych&&this.jsPsych.extensions&&this.jsPsych.extensions["cyborg-hunter"];r&&r.monitor&&(this._chMonitor=r.monitor)}catch{}}_detectHost(){try{var r=this.jsPsych;if(!r||typeof r.version!="function")return null;var t=r.version();return typeof t!="string"||!t?null:{name:"jspsych",version:t}}catch{return null}}on_start(r){}on_load(r){if(this.api){this._monitoring=!0;var t=0,n="unknown";try{t=this.jsPsych.getProgress().current_trial_global}catch{}try{n=(this.jsPsych.getCurrentTrial()||{}).type?.info?.name||"unknown"}catch{}this.api.startTrial({trialId:r&&r.trialId||"trial-"+t,plugin:n})}}on_finish(r){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 r=null;try{r=this._chMonitor?this._chMonitor.getSessionReport():null}catch(n){console.warn("[cyborg-hunter-replay] could not pull CH session report:",n)}var t=await this.api.autoSaveNow({chSessionReport:r,host:this._detectHost()});this._lastRecording=t.recording,this.jsPsych.data.addProperties({integrityReplayMeta:t.meta})}catch(n){console.warn("[cyborg-hunter-replay] finalize failed:",n);try{this.jsPsych.data.addProperties({replayFinalizeError:String(n&&n.message?n.message:n)})}catch{}}try{this.api.destroy()}catch{}this.api=null}}getLastRecording(){return this._lastRecording}};typeof window<"u"&&(window.jsPsychCyborgHunterReplay=Pe);})();
|
|
3
|
+
`)}catch{s=null}r.push(i?{id:n+1,kind:"link",href:i,css:s,media:u}:{id:n+1,kind:"inline",css:s??"",media:u})}return r}function dr(e,r){if(typeof r!="function"||!Array.isArray(e))return Promise.resolve();for(var t=[],n=0;n<e.length;n++){var a=e[n];!a||a.kind!=="link"||a.css!=null||typeof a.href!="string"||!/^https?:/i.test(a.href)||t.push(cr(a,r))}return Promise.all(t).then(function(){})}function cr(e,r){return Promise.resolve().then(function(){return r(e.href,{mode:"cors",credentials:"omit"})}).then(function(t){return!t||!t.ok?null:t.text()}).then(function(t){typeof t=="string"&&e.css==null&&(e.css=t)}).catch(function(){})}function de(e){if(e==null)return 0;try{return JSON.stringify(e).length}catch{return 0}}function pr(e,r){return!e.hasKeyframe||typeof r=="number"&&r>=1&&e.segments>=r?!0:e.lastSnapshotChars>0&&e.patchChars>=e.lastSnapshotChars}function dt(e,r){r=r||{};var t=r.doc||document,n=r.win||window,a=r.now||function(){return performance.now()},i=r.MutationObserver||(typeof MutationObserver<"u"?MutationObserver:null),u=r.span||ee(),s=typeof r.scrolled=="function"?r.scrolled:function(){return null},d={keepBait:e.config.keepBait,redactSelector:e.config.redactSelector,taint:r.taint},g=!1,S=x();function x(){var v=e.config.root;if(typeof v=="string"&&v){var b=null;try{b=t.querySelector(v)}catch{b=null}return b?(g=!0,b):(e.captureFailure("observed_root",new Error('root selector "'+v+'" matched nothing at startSession(); observing document.body instead')),t.body)}return v||t.body}function m(){return g?e.config.root:S&&S.id?"#"+S.id:null}e.setObservedRoot(m());try{var y=fr(t);e.setStylesheets(y);var R=t&&t.defaultView;R&&typeof R.fetch=="function"&&dr(y,R.fetch.bind(R))}catch(v){e.captureFailure("stylesheets",v)}var E={hasKeyframe:!1,segments:0,patchChars:0,lastSnapshotChars:0},O=e.config.keyframeEvery;O!=null&&typeof O!="number"&&console.warn("[cyborg-hunter-replay] keyframeEvery must be a number; got "+typeof O+" ("+JSON.stringify(O)+"). The segment fallback is DISABLED for this recording \u2014 keyframes will be taken on accumulated patch size alone."),e.onTrialStart(function(v){if(v.implicit&&E.hasKeyframe){E.segments++;return}if(!pr(E,e.config.keyframeEvery)){E.segments++;return}try{u.reset();var b=K(S,u,d),l=st(S,u,{win:n,scrolled:s(),keepBait:d.keepBait,redactSelector:d.redactSelector,taint:d.taint}),p=de(b)+de(l),w=e.config.maxCharsPerTrial;w!=null&&p>w?(e.captureFailure("dom_snapshot",new Error("initial DOM keyframe of "+p+" chars exceeds maxCharsPerTrial "+w+"; dropped to bound payload size")),v.initialDom=null,v.initialState=null,u.reset(),B()):(v.initialDom=b,v.initialState=l,e.noteSnapshotChars(v,p),E.hasKeyframe=!0,E.segments=1,E.patchChars=0,E.lastSnapshotChars=p)}catch(I){e.captureFailure("dom_snapshot",I),v.initialDom=null,v.initialState=null,u.reset(),B()}});function B(){E.hasKeyframe=!1,E.segments=0,E.patchChars=0,E.lastSnapshotChars=0}if(i){var A=function(v){try{for(var b=$e(v,{root:S,span:u,t:a(),keepBait:d.keepBait,redactSelector:d.redactSelector,taint:d.taint}),l=0;l<b.length;l++)e.pushRecord(b[l],b[l].t);b.length&&(E.patchChars+=de(b))}catch(p){e.captureFailure("mutations",p)}},M=new i(A);try{M.observe(S,Ze),e.addListener({addEventListener:function(){},removeEventListener:function(){}},"_mutation_observer",M,{_isObserver:!0}),e.addPreCloseFlush(function(){var v=M.takeRecords();v&&v.length&&A(v)})}catch(v){e.captureFailure("mutations",v)}}function V(){var v=K(S,ee(),d),b=e.config.maxCharsPerTrial;return b!=null&&de(v)>b?(e.captureFailure("guard_violation",new Error("pre-scramble DOM snapshot exceeds maxCharsPerTrial "+b+"; withheld")),null):v}if(n.GuardFriction&&typeof n.GuardFriction.onViolation=="function")try{var P=e.getState().state;if((P==="session"||P==="trial")&&typeof n.GuardFriction.getCurrentState=="function"){var k=n.GuardFriction.getCurrentState();k&&k.in_violation&&e.pushGuardViolation({reason:k.current_reason||"unknown",phase:"start",synthesized_at_subscribe:!0,dom_at_subscribe:V()},a())}n.GuardFriction.onViolation(function(v){try{v&&v.phase==="start"?e.pushGuardViolation({reason:v.reason,phase:"start",pre_scramble_dom:V()},a()):v&&v.phase&&e.pushGuardViolation({reason:v.reason,phase:v.phase,duration_ms:v.duration!=null?v.duration:null},a())}catch(b){e.captureFailure("guard_violation",b)}})}catch(v){e.captureFailure("guard_violation",v)}}var Ie="0.9.0";function ct(e){return String(e).replace(/[^a-zA-Z0-9_.-]/g,"_")}var ke=2;function G(e,r){return e==null?null:Math.round((e-r)*10)/10}function z(e,r){return(e||[]).map(function(t){var n=Object.assign({},t);return typeof n.t=="number"&&(n.t=G(n.t,r)),typeof n.start=="number"&&(n.start=G(n.start,r)),n})}function pt(e){return e.slice().sort(function(r,t){return r.t-t.t})}var hr={w:0,h:0,dpr:1,scale:1,offset_x:0,offset_y:0};function vr(e){return e?{w:X(e.w),h:X(e.h),dpr:X(e.dpr,1),scale:X(e.scale,1),offset_x:X(e.offset_x),offset_y:X(e.offset_y)}:Object.assign({},hr)}function X(e,r){return typeof e=="number"&&isFinite(e)?e:r??0}function ht(e,r){r=r||{};var t=e.sessionStart;if(t==null)throw new Error("[cyborg-hunter-replay] cannot serialize before startSession() \u2014 call startSession() first, or the timestamps would be meaningless (Unix epoch).");var n=r.chSessionReport||null,a=t,i=e.trials.map(function(s,d){var g=pt(s.events),S=g.length>0?g[g.length-1].t:s.tLoad;return S>a&&(a=S),s.tEnd!=null&&s.tEnd>a&&(a=s.tEnd),{index:d,label:s.trialId!=null?s.trialId:null,plugin:s.plugin!=null?s.plugin:null,t_start:G(s.tStart,t),t_dom_ready:G(s.tDomReady,t),t_load:G(s.tLoad,t),t_end:G(s.tEnd!=null?s.tEnd:S,t),initial_dom:s.initialDom||null,initial_state:s.initialState||null,events:g.map(function(x){var m=Object.assign({},x);return m.t=G(x.t,t),m}),host_data:s.hostData!=null?s.hostData:null,extensions:s.implicit?{"cyborg-hunter":{implicit:!0}}:null}}),u={replay_version:Ie,tier:e.tier,keys:e.keys,viewport_client:e.viewportClient||null,ch_version:n&&n.libraryVersion||null,preset:n&&n.config?n.config.preset:null,scoring:n?{hard_score:n.hardScore||{},soft_score:n.softScore!=null?n.softScore:null,soft_score_threshold:n.softScoreThreshold!=null?n.softScoreThreshold:null,any_hard_triggered:!!n.anyHardTriggered,trials_completed:n.trialsCompleted!=null?n.trialsCompleted:null}:null,session_signals:n?{sidebar_events:z(n.sidebarEvents,t),ai_extensions_found:z(n.aiExtensionsFound,t),devtools_events:z(n.devToolsEvents,t),window_positions:z(n.windowPositions,t),layout_shifts:z(n.layoutShifts,t),zoom_changes:z(n.zoomChanges,t),extension_injections:z(n.extensionInjections,t)}:null,guard_violations:z(e.guardViolations,t),capture_failures:z(e.captureFailures,t),capture_stopped:!!e.captureStopped,inherited_redaction_taint:!!e.inheritedRedactionTaint};return{schema_version:ke,recorder:{name:"cyborg-hunter-replay",version:Ie},host:r.host||null,participant_id:e.participantId!=null?e.participantId:null,recording_started_at:new Date(e.sessionStartEpoch).toISOString(),recording_started_at_perf:t,user_agent:e.userAgent||"",viewport:vr(e.viewport),observed_root:e.observedRoot||null,stylesheets:e.stylesheets||[],stylesheet_events:[],viewport_changes:pt(e.viewportChanges||[]).map(function(s){var d=Object.assign({},s);return d.t=G(s.t,t),d}),rng:null,rng_calls:null,ended_at_perf:a,end_reason:e.endReason||"aborted",truncated:!!e.captureStopped,extensions:{"cyborg-hunter":u},segments:i}}var mr="https://pipe.jspsych.org/api/data/";function yr(e){var r=e&&e.extensions;return r&&r["cyborg-hunter"]||{}}function vt(e){var r=ct(e.participant_id),t=Date.parse(e.recording_started_at);return r+"-replay-"+t+".json"}function mt(e,r){var t=yr(e);return{schema_version:e.schema_version,tier:t.tier!=null?t.tier:null,saved_to:r,bytes_uncompressed:JSON.stringify(e).length,capture_failures:(t.capture_failures||[]).map(function(n){return n.channel}),capture_stopped:!!e.truncated}}async function yt(e,r){var t=r||{mode:"none"},n=vt(e),a=typeof window<"u"?window:{};if(t.mode==="none")return{saved_to:"none",filename:n};if(t.mode==="datapipe"){if(!t.experimentId)return{saved_to:"failed",filename:n,error:'autoSave.mode "datapipe" requires autoSave.experimentId'};try{var i=JSON.stringify(e);if(a.jsPsychPipe&&typeof a.jsPsychPipe.saveData=="function")await a.jsPsychPipe.saveData(t.experimentId,n,i);else{var u=a.fetch||(typeof fetch<"u"?fetch:null);if(!u)throw new Error("no fetch available for DataPipe POST");var s=await u(mr,{method:"POST",headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify({experimentID:t.experimentId,filename:n,data:i})});if(s&&s.ok===!1)throw new Error("DataPipe HTTP error")}return{saved_to:"datapipe:"+t.experimentId+"/"+n,filename:n}}catch(y){return console.error("[cyborg-hunter-replay] DataPipe autosave failed:",y),{saved_to:"failed",filename:n,error:y&&y.message?y.message:String(y)}}}if(t.mode==="download")try{var d=typeof document<"u"?document:null,g=a.Blob||(typeof Blob<"u"?Blob:null),S=a.URL||(typeof URL<"u"?URL:null);if(!d||!g||!S)throw new Error("download mode needs a browser");var x=new g([JSON.stringify(e)],{type:"application/json"}),m=d.createElement("a");return m.href=S.createObjectURL(x),m.download=n,d.body.appendChild(m),m.click(),d.body.removeChild(m),S.revokeObjectURL(m.href),{saved_to:"download",filename:n}}catch(y){return console.error("[cyborg-hunter-replay] download autosave failed:",y),{saved_to:"failed",filename:n,error:y&&y.message?y.message:String(y)}}return{saved_to:"failed",filename:n,error:'unknown autoSave.mode "'+t.mode+'"'}}async function gt(e){var r=JSON.stringify(e),t=typeof window<"u"?window:{},n=t.CompressionStream||(typeof CompressionStream<"u"?CompressionStream:null),a=t.Blob||(typeof Blob<"u"?Blob:null);if(!a)return null;if(!n)return new a([r],{type:"application/json"});var i=new a([r]).stream().pipeThrough(new n("gzip")),u=t.Response||(typeof Response<"u"?Response:null),s=await new u(i).arrayBuffer();return new a([s],{type:"application/gzip"})}var Y=null;function Ne(e){Y&&(console.warn("[cyborg-hunter-replay] attach() called while a recorder is active \u2014 replacing the previous instance (its unsaved recording is discarded)."),Y.destroy(),Y=null);var r=je(e),t={_recorder:r,config:r.config,startSession:function(){r.startSession();var n=ee(),a=Qe(r,{span:n});return(r.config.tier==="dom"||r.config.tier==="canvas")&&dt(r,{span:n,scrolled:a.getScrolledElements}),t},startTrial:function(n){return r.startTrial(n),t},endTrial:function(){return r.endTrial(),t},stopSession:function(n){return r.stopSession(n),t},getRecording:function(n){return ht(r.getState(),n||{})},getRecordingCompressed:function(n){return gt(t.getRecording(n))},autoSaveNow:async function(n){if(r.getState().sessionStart==null)return{recording:null,saveResult:{saved_to:"no_session"},meta:{schema_version:ke,saved_to:"no_session",note:"recorder was never started; nothing to save"}};var a=t.getRecording(n),i=await yt(a,r.config.autoSave),u=mt(a,i.saved_to);return i.error&&(u.save_error=i.error),{recording:a,saveResult:i,meta:u}},destroy:function(){r.destroy(),Y===t&&(Y=null)}};return Y=t,t}typeof window<"u"&&(window.CyborgHunterReplay={attach:Ne});var Pe=class{static info={name:"cyborg-hunter-replay",version:"0.9.0",data:{}};constructor(r){this.jsPsych=r,this.api=null,this._chMonitor=null,this._lastRecording=null,this._monitoring=!1}initialize(r){this.params=r||{},this.params.keyframeEvery!=null&&this.params.keyframeEvery!==1&&console.warn("[cyborg-hunter-replay] ignoring keyframeEvery: "+this.params.keyframeEvery+" \u2014 jsPsych wipes the display between trials, so every trial segment is recorded as a keyframe."),this.api=Ne(Object.assign({},this.params,{keyframeEvery:1})),this.api.startSession(),this._stashChMonitor()}_stashChMonitor(){try{var r=this.jsPsych&&this.jsPsych.extensions&&this.jsPsych.extensions["cyborg-hunter"];r&&r.monitor&&(this._chMonitor=r.monitor)}catch{}}_detectHost(){try{var r=this.jsPsych;if(!r||typeof r.version!="function")return null;var t=r.version();return typeof t!="string"||!t?null:{name:"jspsych",version:t}}catch{return null}}on_start(r){}on_load(r){if(this.api){this._monitoring=!0;var t=0,n="unknown";try{t=this.jsPsych.getProgress().current_trial_global}catch{}try{n=(this.jsPsych.getCurrentTrial()||{}).type?.info?.name||"unknown"}catch{}this.api.startTrial({trialId:r&&r.trialId||"trial-"+t,plugin:n})}}on_finish(r){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 r=null;try{r=this._chMonitor?this._chMonitor.getSessionReport():null}catch(n){console.warn("[cyborg-hunter-replay] could not pull CH session report:",n)}var t=await this.api.autoSaveNow({chSessionReport:r,host:this._detectHost()});this._lastRecording=t.recording,this.jsPsych.data.addProperties({integrityReplayMeta:t.meta})}catch(n){console.warn("[cyborg-hunter-replay] finalize failed:",n);try{this.jsPsych.data.addProperties({replayFinalizeError:String(n&&n.message?n.message:n)})}catch{}}try{this.api.destroy()}catch{}this.api=null}}getLastRecording(){return this._lastRecording}};typeof window<"u"&&(window.jsPsychCyborgHunterReplay=Pe);})();
|
|
@@ -32,7 +32,7 @@ function deepCopy(obj) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// src/shared/constants.js
|
|
35
|
-
var VERSION = "0.
|
|
35
|
+
var VERSION = "0.9.0";
|
|
36
36
|
var DEFAULT_THRESHOLDS = {
|
|
37
37
|
pasteMinChars: 0,
|
|
38
38
|
// record ALL pastes (was 20; now 0 by default)
|
|
@@ -234,6 +234,8 @@ var DEFAULT_CLI_CONFIG = {
|
|
|
234
234
|
dataDir: "./data",
|
|
235
235
|
replayDir: null,
|
|
236
236
|
// replay artifacts dir; defaults to dataDir
|
|
237
|
+
scoreWeights: null,
|
|
238
|
+
// report-score weights, merged per key onto the defaults (cli/analyzers/score-weights.js)
|
|
237
239
|
filePattern: "*.json",
|
|
238
240
|
participantIdField: "participantId",
|
|
239
241
|
trialIdField: "trialId",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// cyborg-hunter v0.
|
|
2
|
-
var CyborgHunter=(()=>{var _=Object.defineProperty;var Te=Object.getOwnPropertyDescriptor;var be=Object.getOwnPropertyNames;var Ee=Object.prototype.hasOwnProperty;var Ie=(e,n)=>{for(var t in n)_(e,t,{get:n[t],enumerable:!0})},Me=(e,n,t,o)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of be(n))!Ee.call(e,r)&&r!==t&&_(e,r,{get:()=>n[r],enumerable:!(o=Te(n,r))||o.enumerable});return e};var xe=e=>Me(_({},"__esModule",{value:!0}),e);var Fe={};Ie(Fe,{VERSION:()=>q,addHoneypot:()=>me,init:()=>we,preventTextSelection:()=>ve,setAltText:()=>ye});var W={created:["session","destroyed"],session:["trial","destroyed"],trial:["session","destroyed"],destroyed:[]};function J(){let e="created";return{get current(){return e},transition(n){return W[e]&&W[e].indexOf(n)!==-1?(e=n,!0):(console.warn("[cyborg-hunter] invalid transition: "+e+" \u2192 "+n),!1)}}}function N(e){if(e===null||typeof e!="object")return e;try{return JSON.parse(JSON.stringify(e))}catch{return e}}var q="0.8.0",z={pasteMinChars:0,dropMinChars:0,sidebarGapPx:100,layoutCompressionPx:20,viewportShiftDebounceMs:250,syntheticGapMs:100,idleGapMs:1e4,idleCheckIntervalMs:5e3,mouseThrottleMs:50,mouseMaxEvents:2e3,mouseBotMinEvents:3,sidebarPollMs:2e3,extensionScanMs:3e4,windowPositionPollMs:2e3,elementTraceHz:500,tabAwayDurationMs:3e3,typingSpeedCps:10},$={permissive:{signals:{paste:!0,copy:!0,drop:!0,tabAway:!0,typingSpeed:!0,devTools:!0,aiExtensions:!0,sidebarGap:!0,keyboardShortcuts:!0,mouseTracking:!0,idleGaps:!0,windowPosition:!0,clipboardManager:!0,keystrokeDynamics:!1},thresholds:{typingSpeedCps:15},scoring:{hard:{paste:{countThreshold:3},drop:{countThreshold:3}},soft:{copy:{weight:1,maxPerTrial:2},tabAway:{weight:1,maxPerTrial:2},typingSpeed:{weight:1},sidebarEvent:{weight:1},devTools:{weight:1},foreignInput:{weight:1}},softScoreThreshold:10},screenout:{enabled:!1,gracePeriodTrials:5}},standard:{signals:{paste:!0,copy:!0,drop:!0,tabAway:!0,typingSpeed:!0,devTools:!0,aiExtensions:!0,sidebarGap:!0,keyboardShortcuts:!0,mouseTracking:!0,idleGaps:!0,windowPosition:!0,clipboardManager:!0,keystrokeDynamics:!1},thresholds:{},scoring:{hard:{paste:{countThreshold:2},drop:{countThreshold:2}},soft:{copy:{weight:2,maxPerTrial:2},tabAway:{weight:1,maxPerTrial:2},typingSpeed:{weight:2},sidebarEvent:{weight:3},devTools:{weight:1},foreignInput:{weight:2}},softScoreThreshold:6},screenout:{enabled:!0,gracePeriodTrials:3}},strict:{signals:{paste:!0,copy:!0,drop:!0,tabAway:!0,typingSpeed:!0,devTools:!0,aiExtensions:!0,sidebarGap:!0,keyboardShortcuts:!0,mouseTracking:!0,idleGaps:!0,windowPosition:!0,clipboardManager:!0,keystrokeDynamics:!0},thresholds:{typingSpeedCps:8,tabAwayDurationMs:5e3,mouseMaxEvents:5e3},scoring:{hard:{paste:{countThreshold:1},copy:{countThreshold:2},drop:{countThreshold:1}},soft:{tabAway:{weight:2,maxPerTrial:1},typingSpeed:{weight:3},sidebarEvent:{weight:2},devTools:{weight:2},foreignInput:{weight:3}},softScoreThreshold:4},screenout:{enabled:!0,gracePeriodTrials:2}}},X=[{name:"ChatGPT sidebar",sel:"chatgpt-sidebar"},{name:"ChatGPT extension",sel:"[data-chatgpt]"},{name:"Copilot extension",sel:"copilot-suggestions"},{name:"Copilot extension",sel:"[data-copilot]"},{name:"Gemini extension",sel:"gemini-panel"},{name:"Claude extension",sel:"[data-claude-extension]"},{name:"Monica AI",sel:"monica-root"},{name:"Merlin AI",sel:"merlin-root"},{name:"Sider AI",sel:"[data-sider-extension]"},{name:"MaxAI",sel:"[data-maxai]"},{name:"AI assistant",sel:"[data-ai-assistant]"},{name:"AI iframe (OpenAI)",sel:'iframe[src*="chat.openai.com"]'},{name:"AI iframe (Copilot)",sel:'iframe[src*="copilot.microsoft.com"]'},{name:"AI iframe (Gemini)",sel:'iframe[src*="gemini.google.com"]'},{name:"AI iframe (Claude)",sel:'iframe[src*="claude.ai"]'},{name:"AI iframe (Perplexity)",sel:'iframe[src*="perplexity.ai"]'}],Y=["grammarly","lastpass","1password","bitwarden","dashlane","roboform","honey","ublock","adblock"];var Q={dataDir:"./data",replayDir:null,filePattern:"*.json",participantIdField:"participantId",trialIdField:"trialId",trialOrderField:"trialIndex",integrityField:"integrity",sessionIntegrityPath:null,trialsPerParticipant:null,platformIdField:null,showPlatformId:!1,conditionField:null,groupField:null,typingSpeedThreshold_cps:10,tabAwayMinDuration_ms:3e3,idleGapThreshold_ms:1e4,suspiciouslyFastRT_ms:2e3,scoring:null,signals:null,phaseScope:null,trajectoryGrid:"auto",trajectoryTrialLabel:"trialId",trajectoryResponseField:null,trajectoryDisplayOrder:"rule",outputDir:"./cyborg-hunter-report",outputFormat:"html"};var Ce=["replayDir",...Object.keys(Q),...Object.keys(z),"preset","participantId","signals","thresholds","scoring","domProtection","collectForPostHoc","onSignal","screenout","experimentContainer","knownInputs","decoyAnswers","decoyMap","decoyVisibility","decoyFraming","decoyExcludeButtons","autoMonitor","excludeTrialTypes"];function ee(e,n=Ce){let t=[];return Object.keys(e).forEach(o=>{if(!n.includes(o)){let r=Ae(o,n),i=r?`Unknown config key "${o}" \u2014 did you mean "${r}"?`:`Unknown config key "${o}"`;t.push(i)}}),t}function te(e){let n=[];if(!e||typeof e!="object")return n;let t=e.hard||{};Object.keys(t).forEach(r=>{let i=t[r];(!i||!Number.isFinite(i.countThreshold))&&n.push(`scoring.hard.${r} has no finite numeric countThreshold (got ${i?Z(i.countThreshold):"no rule"}) \u2014 this hard screenout rule can never trigger. A likely cause is a typo in the override key (e.g. "countThreshhold").`)});let o=e.soft||{};return Object.keys(o).forEach(r=>{let i=o[r];(!i||!Number.isFinite(i.weight))&&n.push(`scoring.soft.${r} has no finite numeric weight (got ${i?Z(i.weight):"no rule"}) \u2014 this soft signal contributes nothing (or breaks the soft score). A likely cause is a typo in the override key (e.g. "wieght").`)}),n}function Z(e){try{return String(e)}catch{return typeof e}}function Ae(e,n){let t=null,o=1/0;for(let r of n){let i=Pe(e.toLowerCase(),r.toLowerCase());i<o&&i<=3&&(o=i,t=r)}return t}function Pe(e,n){let t=e.length,o=n.length,r=Array.from({length:t+1},()=>Array(o+1).fill(0));for(let i=0;i<=t;i++)r[i][0]=i;for(let i=0;i<=o;i++)r[0][i]=i;for(let i=1;i<=t;i++)for(let a=1;a<=o;a++)r[i][a]=e[i-1]===n[a-1]?r[i-1][a-1]:1+Math.min(r[i-1][a],r[i][a-1],r[i-1][a-1]);return r[t][o]}function re(e,n,t,o){var r=t.scoring,i={hard:{},soft:{}},a=0;if(r.hard.paste){var c=(e.pasteEvents||[]).length;i.hard.paste={trialHits:c,sessionTotal:n.pasteCount,countThreshold:r.hard.paste.countThreshold}}if(r.hard.copy){var v=(e.copyEvents||[]).length;i.hard.copy={trialHits:v,sessionTotal:n.copyCount,countThreshold:r.hard.copy.countThreshold}}if(r.hard.drop){var d=(e.dropEvents||[]).length;i.hard.drop={trialHits:d,sessionTotal:n.dropCount,countThreshold:r.hard.drop.countThreshold}}if(r.soft.copy){var h=(e.copyEvents||[]).length,m=r.soft.copy.maxPerTrial!=null?Math.min(h,r.soft.copy.maxPerTrial):h,T=m*r.soft.copy.weight;a+=T,i.soft.copy={hits:h,capped:m,score:T}}if(r.soft.tabAway){var y=(e.tabAwayEvents||[]).filter(function(F){return(F.duration_ms||0)>t.thresholds.tabAwayDurationMs}).length,k=r.soft.tabAway.maxPerTrial!=null?Math.min(y,r.soft.tabAway.maxPerTrial):y,M=k*r.soft.tabAway.weight;a+=M,i.soft.tabAway={hits:y,capped:k,score:M}}if(r.soft.typingSpeed&&o.charsPerSec!=null){var b=t.thresholds.typingSpeedCps,E=o.charsPerSec>b?1:0,u=E*r.soft.typingSpeed.weight;a+=u,i.soft.typingSpeed={charsPerSec:o.charsPerSec,threshold:b,hit:E,score:u}}var p=e.startTime+(o&&o.duration_ms!=null?o.duration_ms:performance.now()-e.startTime);if(r.soft.sidebarEvent){var S=n.sidebarEvents.filter(function(F){return F.t>=e.startTime&&F.t<=p}).length;if(S>0){var l=r.soft.sidebarEvent.weight;a+=l,i.soft.sidebarEvent={hits:S,score:l}}}if(r.soft.devTools){var f=n.keyboardShortcuts.filter(function(F){return F.t>=e.startTime&&F.t<=p}).length;if(f>0){var g=r.soft.devTools.weight;a+=g,i.soft.devTools={hits:f,score:g}}}if(r.soft.foreignInput&&e.foreignInputEvents.length>0){var I=e.foreignInputEvents.length,D=r.soft.foreignInput.weight;a+=D,i.soft.foreignInput={hits:I,score:D}}for(var P={},L=Object.keys(r.hard),j=0;j<L.length;j++){var C=L[j],K=C==="paste"?n.pasteCount:C==="copy"?n.copyCount:C==="drop"?n.dropCount:0;P[C]={count:K,threshold:r.hard[C].countThreshold,triggered:K>=r.hard[C].countThreshold}}return{trialSoftScore:a,trialSignals:i,updatedHardScore:P}}function ne(e,n){if(!n.screenout.enabled)return!1;var t=Object.keys(e.hardScore).some(function(o){return e.hardScore[o].triggered});return t?!0:e.trialsCompleted<n.screenout.gracePeriodTrials?!1:e.softScore>=n.scoring.softScoreThreshold}function oe(e){var n=e.config,t=e.trialData;n.signals.paste&&e.addTrialListener(document,"paste",function(o){var r="";try{r=(o.clipboardData||window.clipboardData).getData("text")}catch{}var i=r.length;if(i>=n.thresholds.pasteMinChars){var a=e.isKnownInput(o.target),c={type:"paste",t:performance.now(),pastedLength:i,isKnownInput:a};n.collectForPostHoc.pasteDropContent&&(c.text=r),t.pasteEvents.push(c),e.sessionData.pasteCount++,e.fireSignal("paste",o,{pastedLength:i,isKnownInput:a,text:r})}}),n.signals.copy&&(e.addTrialListener(document,"copy",function(o){var r=window.getSelection(),i=r?r.toString():"",a=i.length;t.copyEvents.push({type:"copy",t:performance.now(),selectedLength:a}),e.sessionData.copyCount++,e.fireSignal("copy",o,{selectedLength:a})}),e.addTrialListener(document,"cut",function(o){t.copyEvents.push({type:"cut",t:performance.now()}),e.sessionData.copyCount++,e.fireSignal("cut",o,{})})),n.signals.drop&&e.addTrialListener(document,"drop",function(o){var r="";try{r=o.dataTransfer.getData("text")}catch{}var i=r.length;if(i>=n.thresholds.dropMinChars){var a=e.isKnownInput(o.target),c={type:"drop",t:performance.now(),droppedLength:i,isKnownInput:a};n.collectForPostHoc.pasteDropContent&&(c.text=r),t.dropEvents.push(c),e.sessionData.dropCount++,e.fireSignal("drop",o,{droppedLength:i,isKnownInput:a,text:r})}})}function ae(e){var n=e.config;if(n.signals.tabAway){let r=function(a){t===null&&(t=performance.now(),o=a,e.fireSignal("tabAway",null,{type:a}))},i=function(){if(t!==null){var a=performance.now()-t,c={start:t,duration_ms:Math.round(a),type:o,timestamp:new Date(Date.now()-a).toISOString()};e.getTrialData()&&e.getTrialData().tabAwayEvents.push(c),e.sessionData.tabAwayEvents.push(c),e.sessionData.tabAwaySums.push(Math.round(a)),e.fireSignal("tabReturn",null,{duration_ms:Math.round(a),type:o}),t=null,o=null}};var t=null,o=null;e.addListener(document,"visibilitychange",function(){document.hidden?r("tabHidden"):i()}),e.addListener(window,"blur",function(){r("windowBlur")}),e.addListener(window,"focus",function(){document.hidden||i()})}}function ie(e){var n=e.config,t=e.trialData;if(n.signals.idleGaps){var o=n.thresholds.idleGapMs,r=performance.now();e.addTrialListener(document,"keydown",function(){r=performance.now()}),e.addTrialListener(document,"mousemove",function(){r=performance.now()},{passive:!0});var i=setInterval(function(){if(e.getTrialData()){var a=performance.now()-r;a>o&&(t.idleGaps.push({duration_ms:Math.round(a),t:performance.now()}),r=performance.now())}},n.thresholds.idleCheckIntervalMs);e.addTrialInterval(i)}}function se(e){var n=e.config,t=e.trialData;if(n.signals.mouseTracking){let c=function(v){return function(d){t.mouseEvents.length>=r||t.mouseEvents.push({x:Math.round(d.pageX),y:Math.round(d.pageY),t:Math.round(performance.now()-a),type:v})}};var o=n.thresholds.mouseThrottleMs,r=n.thresholds.mouseMaxEvents,i=0,a=t.startTime;t.mouseTrackingCapped=!1,t.mouseTrackingCappedAtMs=null,e.addTrialListener(document,"mousemove",function(v){if(t.mouseEvents.length>=r){t.mouseTrackingCapped||(t.mouseTrackingCapped=!0,t.mouseTrackingCappedAtMs=Math.round(performance.now()-a));return}var d=performance.now();d-i<o||(i=d,t.mouseEvents.push({x:Math.round(v.pageX),y:Math.round(v.pageY),t:Math.round(d-a),type:"move"}))},{passive:!0}),e.addTrialListener(document,"click",c("click"),{passive:!0}),e.addTrialListener(document,"mousedown",c("down"),{passive:!0}),e.addTrialListener(document,"mouseup",c("up"),{passive:!0})}}function le(e,n){var t=e.filter(function(g){return g.type==="move"});if(t.length<n)return null;for(var o=0,r=[],i=0,a=0,c=0,v=0,d=1;d<t.length;d++){var h=t[d].x-t[d-1].x,m=t[d].y-t[d-1].y,T=Math.sqrt(h*h+m*m);o+=T;var y=t[d].t-t[d-1].t;y>0&&r.push(T/y),d>1&&((h>0&&c<0||h<0&&c>0)&&i++,(m>0&&v<0||m<0&&v>0)&&a++),c=h,v=m}for(var k=t[0],M=t[t.length-1],b=Math.sqrt(Math.pow(M.x-k.x,2)+Math.pow(M.y-k.y,2)),E=o>0?Math.round(b/o*1e3)/1e3:0,u=0,p=0,S=0;S<r.length;S++){var l=r[S]-u;u+=l/(S+1),p+=l*(r[S]-u)}var f=r.length>1?Math.round(p/(r.length-1)*1e4)/1e4:0;return{pathEfficiency:E,directionChanges:i+a,speedVariance:f,moveCount:t.length}}function ce(e){var n=e.config,t=e.trialData;if(n.signals.clipboardManager){var o=0;e.addTrialListener(document,"keydown",function(){o=performance.now()}),e.addTrialListener(document,"input",function(r){if(r.target&&(r.target.tagName==="INPUT"||r.target.tagName==="TEXTAREA"||r.target.isContentEditable)&&(t.editTimestamps.push(performance.now()),performance.now()-o>n.thresholds.syntheticGapMs&&r.inputType==="insertText")){var i=(r.data||"").length;t.syntheticInsertions.push({type:"synthetic_insertion",t:performance.now(),dataLength:i}),e.fireSignal("syntheticInsertion",r,{dataLength:i})}},!0)}n.signals.typingSpeed&&!n.signals.clipboardManager&&e.addTrialListener(document,"input",function(r){r.target&&(r.target.tagName==="INPUT"||r.target.tagName==="TEXTAREA"||r.target.isContentEditable)&&t.editTimestamps.push(performance.now())},!0)}function ue(e){var n=e.config,t=e.trialData;e.addTrialListener(document,"input",function(o){var r=o.target;if(r){var i=r.tagName==="INPUT"||r.tagName==="TEXTAREA"||r.isContentEditable;if(i){var a=e.isKnownInput(r);if(a===!1){var c={t:performance.now(),targetTag:r.tagName||"unknown",targetId:(r.id||"").slice(0,50),targetClass:(r.className||"").toString().slice(0,100),inputType:o.inputType||""};n.collectForPostHoc.foreignInputContent&&(c.data=o.data||""),t.foreignInputEvents.push(c),e.fireSignal("typingOutsideExperiment",o,{targetTag:c.targetTag,targetId:c.targetId,targetClass:c.targetClass,data:o.data||""})}}}},!0)}function de(e){if(e.length<2)return null;var n=e[0],t=e[e.length-1],o=(t-n)/1e3;if(o<=0)return null;var r=e.length;return Math.round(r/o*10)/10}function pe(e){var n=e.config,t=e.sessionData;if(n.signals.sidebarGap){var o=window.devicePixelRatio||1,r=window.innerWidth,i=!1,a=null,c=null,v=0,d=null,h=!1,m=null;e.addInterval(setInterval(function(){var l=window.innerWidth,f=n.thresholds.sidebarGapPx,g=r-l;if(!i&&g>f)i=!0,a=performance.now(),c="innerWidth_delta",v=g,d=l,t.sidebarEvents.push({type:"opened",method:"innerWidth_delta",deltaIW:g,innerWidth:l,baselineIW:r,t:performance.now()}),e.fireSignal("sidebarOpened",null,{deltaIW:g});else if(i&&c==="innerWidth_delta"&&g<f/2){var I=performance.now()-a,D=d!=null?d-l:null;t.sidebarEvents.push({type:"closed",method:"innerWidth_delta",deltaIW:D,duration_ms:Math.round(I),t:performance.now()}),e.fireSignal("sidebarClosed",null,{duration_ms:Math.round(I),deltaIW:D}),i=!1,a=null,d=null,r=l}else i||(r=l);var P=l-document.documentElement.clientWidth,L=n.thresholds.layoutCompressionPx;if(!h&&P>L)h=!0,m=performance.now(),t.sidebarEvents.push({type:"opened",method:"layout_compression",gap:P,t:performance.now()});else if(h&&P<=L){var j=performance.now()-m;t.sidebarEvents.push({type:"closed",method:"layout_compression",duration_ms:Math.round(j),t:performance.now()}),h=!1,m=null}var C=window.devicePixelRatio||1;C!==o&&(t.zoomChanges.push({from:o,to:C,t:performance.now()}),r=l,o=C)},n.thresholds.sidebarPollMs))}if(n.signals.aiExtensions){let l=function(){X.forEach(function(f){if(document.querySelector(f.sel)){var g=t.aiExtensionsFound.some(function(I){return I.name===f.name});g||t.aiExtensionsFound.push({name:f.name,t:performance.now()})}})};l(),e.addInterval(setInterval(l,n.thresholds.extensionScanMs))}if((n.signals.keyboardShortcuts||n.signals.devTools)&&e.addListener(document,"keydown",function(l){var f=l.ctrlKey||l.metaKey;if(f&&l.shiftKey&&(l.key==="I"||l.key==="J"||l.key==="C")){var g="Ctrl+Shift+"+l.key;t.keyboardShortcuts.push({combo:g,t:performance.now()}),e.fireSignal("keyboardShortcut",l,{combo:g})}l.key==="F12"&&(t.keyboardShortcuts.push({combo:"F12",t:performance.now()}),e.fireSignal("keyboardShortcut",l,{combo:"F12"}))}),n.signals.windowPosition&&n.collectForPostHoc.windowPositionLog){var T=function(){t.windowPositions.push({x:window.screenX,y:window.screenY,w:window.outerWidth,h:window.outerHeight,iw:window.innerWidth,ih:window.innerHeight,sw:window.screen.width,sh:window.screen.height,dpr:window.devicePixelRatio,vvScale:window.visualViewport?window.visualViewport.scale:null,t:performance.now()})};e.addInterval(setInterval(T,n.thresholds.windowPositionPollMs));var y=null,k=function(){y&&clearTimeout(y),y=setTimeout(function(){T(),y=null},250)};e.addListener(window,"resize",k)}if(n.signals.aiExtensions){let l=function(f){var g=f.toLowerCase();return Y.some(function(I){return g.indexOf(I)!==-1})};var M=new MutationObserver(function(f){for(var g=0;g<f.length;g++)for(var I=f[g].addedNodes,D=0;D<I.length;D++){var P=I[D];if(P.nodeType===Node.ELEMENT_NODE){var L=P.tagName||"";L.indexOf("-")!==-1&&!l(L)&&t.extensionInjections.push({tag:L.toLowerCase(),hasShadow:!!P.shadowRoot,t:performance.now()})}}});M.observe(document.body,{childList:!0,subtree:!1}),e.listeners.push({target:null,event:"mutation",handler:M,options:{_isMutationObserver:!0}})}if(n.signals.sidebarGap){var b=document.documentElement.clientWidth,E=null,u=null,p=function(){if(u=null,E!==null){var l=E;E=null;var f=b-l;Math.abs(f)>n.thresholds.layoutCompressionPx&&(t.viewportWidthShifts.push({oldWidth:Math.round(b),newWidth:Math.round(l),delta:Math.round(f),t:performance.now()}),b=l)}},S=new ResizeObserver(function(l){for(var f=0;f<l.length;f++)E=l[f].contentRect.width;u&&clearTimeout(u),u=setTimeout(p,n.thresholds.viewportShiftDebounceMs),e.addInterval(u)});S.observe(document.documentElement),e.listeners.push({target:null,event:"resize",handler:S,options:{_isResizeObserver:!0}})}}function fe(e){var n=e.config,t=e.trialData;if(n.collectForPostHoc.elementTrace){t.elementTrace=[];var o=0,r=0;e.addTrialListener(document,"mousemove",function(a){o=a.clientX,r=a.clientY},{passive:!0});var i=setInterval(function(){if(e.getTrialData()&&!(o===0&&r===0))try{var a=document.elementsFromPoint(o,r);if(a&&a.length>0){var c=a[0];t.elementTrace.push({tag:c.tagName.toLowerCase(),id:c.id||null,t:Math.round(performance.now()-t.startTime)})}}catch{}},n.thresholds.elementTraceHz);e.addTrialInterval(i)}}function R(e,n,t,o){return t==="C"?e:n==="calibration-metadata"?"Validation trial (integrity check, ID: cal-"+o+`).
|
|
1
|
+
// cyborg-hunter v0.9.0 — https://github.com/cyborg-hunter/cyborg-hunter
|
|
2
|
+
var CyborgHunter=(()=>{var _=Object.defineProperty;var Te=Object.getOwnPropertyDescriptor;var be=Object.getOwnPropertyNames;var Ee=Object.prototype.hasOwnProperty;var Ie=(e,n)=>{for(var t in n)_(e,t,{get:n[t],enumerable:!0})},Me=(e,n,t,o)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of be(n))!Ee.call(e,r)&&r!==t&&_(e,r,{get:()=>n[r],enumerable:!(o=Te(n,r))||o.enumerable});return e};var xe=e=>Me(_({},"__esModule",{value:!0}),e);var Fe={};Ie(Fe,{VERSION:()=>q,addHoneypot:()=>me,init:()=>we,preventTextSelection:()=>ve,setAltText:()=>ye});var W={created:["session","destroyed"],session:["trial","destroyed"],trial:["session","destroyed"],destroyed:[]};function J(){let e="created";return{get current(){return e},transition(n){return W[e]&&W[e].indexOf(n)!==-1?(e=n,!0):(console.warn("[cyborg-hunter] invalid transition: "+e+" \u2192 "+n),!1)}}}function N(e){if(e===null||typeof e!="object")return e;try{return JSON.parse(JSON.stringify(e))}catch{return e}}var q="0.9.0",z={pasteMinChars:0,dropMinChars:0,sidebarGapPx:100,layoutCompressionPx:20,viewportShiftDebounceMs:250,syntheticGapMs:100,idleGapMs:1e4,idleCheckIntervalMs:5e3,mouseThrottleMs:50,mouseMaxEvents:2e3,mouseBotMinEvents:3,sidebarPollMs:2e3,extensionScanMs:3e4,windowPositionPollMs:2e3,elementTraceHz:500,tabAwayDurationMs:3e3,typingSpeedCps:10},$={permissive:{signals:{paste:!0,copy:!0,drop:!0,tabAway:!0,typingSpeed:!0,devTools:!0,aiExtensions:!0,sidebarGap:!0,keyboardShortcuts:!0,mouseTracking:!0,idleGaps:!0,windowPosition:!0,clipboardManager:!0,keystrokeDynamics:!1},thresholds:{typingSpeedCps:15},scoring:{hard:{paste:{countThreshold:3},drop:{countThreshold:3}},soft:{copy:{weight:1,maxPerTrial:2},tabAway:{weight:1,maxPerTrial:2},typingSpeed:{weight:1},sidebarEvent:{weight:1},devTools:{weight:1},foreignInput:{weight:1}},softScoreThreshold:10},screenout:{enabled:!1,gracePeriodTrials:5}},standard:{signals:{paste:!0,copy:!0,drop:!0,tabAway:!0,typingSpeed:!0,devTools:!0,aiExtensions:!0,sidebarGap:!0,keyboardShortcuts:!0,mouseTracking:!0,idleGaps:!0,windowPosition:!0,clipboardManager:!0,keystrokeDynamics:!1},thresholds:{},scoring:{hard:{paste:{countThreshold:2},drop:{countThreshold:2}},soft:{copy:{weight:2,maxPerTrial:2},tabAway:{weight:1,maxPerTrial:2},typingSpeed:{weight:2},sidebarEvent:{weight:3},devTools:{weight:1},foreignInput:{weight:2}},softScoreThreshold:6},screenout:{enabled:!0,gracePeriodTrials:3}},strict:{signals:{paste:!0,copy:!0,drop:!0,tabAway:!0,typingSpeed:!0,devTools:!0,aiExtensions:!0,sidebarGap:!0,keyboardShortcuts:!0,mouseTracking:!0,idleGaps:!0,windowPosition:!0,clipboardManager:!0,keystrokeDynamics:!0},thresholds:{typingSpeedCps:8,tabAwayDurationMs:5e3,mouseMaxEvents:5e3},scoring:{hard:{paste:{countThreshold:1},copy:{countThreshold:2},drop:{countThreshold:1}},soft:{tabAway:{weight:2,maxPerTrial:1},typingSpeed:{weight:3},sidebarEvent:{weight:2},devTools:{weight:2},foreignInput:{weight:3}},softScoreThreshold:4},screenout:{enabled:!0,gracePeriodTrials:2}}},X=[{name:"ChatGPT sidebar",sel:"chatgpt-sidebar"},{name:"ChatGPT extension",sel:"[data-chatgpt]"},{name:"Copilot extension",sel:"copilot-suggestions"},{name:"Copilot extension",sel:"[data-copilot]"},{name:"Gemini extension",sel:"gemini-panel"},{name:"Claude extension",sel:"[data-claude-extension]"},{name:"Monica AI",sel:"monica-root"},{name:"Merlin AI",sel:"merlin-root"},{name:"Sider AI",sel:"[data-sider-extension]"},{name:"MaxAI",sel:"[data-maxai]"},{name:"AI assistant",sel:"[data-ai-assistant]"},{name:"AI iframe (OpenAI)",sel:'iframe[src*="chat.openai.com"]'},{name:"AI iframe (Copilot)",sel:'iframe[src*="copilot.microsoft.com"]'},{name:"AI iframe (Gemini)",sel:'iframe[src*="gemini.google.com"]'},{name:"AI iframe (Claude)",sel:'iframe[src*="claude.ai"]'},{name:"AI iframe (Perplexity)",sel:'iframe[src*="perplexity.ai"]'}],Y=["grammarly","lastpass","1password","bitwarden","dashlane","roboform","honey","ublock","adblock"];var Q={dataDir:"./data",replayDir:null,scoreWeights:null,filePattern:"*.json",participantIdField:"participantId",trialIdField:"trialId",trialOrderField:"trialIndex",integrityField:"integrity",sessionIntegrityPath:null,trialsPerParticipant:null,platformIdField:null,showPlatformId:!1,conditionField:null,groupField:null,typingSpeedThreshold_cps:10,tabAwayMinDuration_ms:3e3,idleGapThreshold_ms:1e4,suspiciouslyFastRT_ms:2e3,scoring:null,signals:null,phaseScope:null,trajectoryGrid:"auto",trajectoryTrialLabel:"trialId",trajectoryResponseField:null,trajectoryDisplayOrder:"rule",outputDir:"./cyborg-hunter-report",outputFormat:"html"};var Ce=["replayDir",...Object.keys(Q),...Object.keys(z),"preset","participantId","signals","thresholds","scoring","domProtection","collectForPostHoc","onSignal","screenout","experimentContainer","knownInputs","decoyAnswers","decoyMap","decoyVisibility","decoyFraming","decoyExcludeButtons","autoMonitor","excludeTrialTypes"];function ee(e,n=Ce){let t=[];return Object.keys(e).forEach(o=>{if(!n.includes(o)){let r=Ae(o,n),i=r?`Unknown config key "${o}" \u2014 did you mean "${r}"?`:`Unknown config key "${o}"`;t.push(i)}}),t}function te(e){let n=[];if(!e||typeof e!="object")return n;let t=e.hard||{};Object.keys(t).forEach(r=>{let i=t[r];(!i||!Number.isFinite(i.countThreshold))&&n.push(`scoring.hard.${r} has no finite numeric countThreshold (got ${i?Z(i.countThreshold):"no rule"}) \u2014 this hard screenout rule can never trigger. A likely cause is a typo in the override key (e.g. "countThreshhold").`)});let o=e.soft||{};return Object.keys(o).forEach(r=>{let i=o[r];(!i||!Number.isFinite(i.weight))&&n.push(`scoring.soft.${r} has no finite numeric weight (got ${i?Z(i.weight):"no rule"}) \u2014 this soft signal contributes nothing (or breaks the soft score). A likely cause is a typo in the override key (e.g. "wieght").`)}),n}function Z(e){try{return String(e)}catch{return typeof e}}function Ae(e,n){let t=null,o=1/0;for(let r of n){let i=Pe(e.toLowerCase(),r.toLowerCase());i<o&&i<=3&&(o=i,t=r)}return t}function Pe(e,n){let t=e.length,o=n.length,r=Array.from({length:t+1},()=>Array(o+1).fill(0));for(let i=0;i<=t;i++)r[i][0]=i;for(let i=0;i<=o;i++)r[0][i]=i;for(let i=1;i<=t;i++)for(let a=1;a<=o;a++)r[i][a]=e[i-1]===n[a-1]?r[i-1][a-1]:1+Math.min(r[i-1][a],r[i][a-1],r[i-1][a-1]);return r[t][o]}function re(e,n,t,o){var r=t.scoring,i={hard:{},soft:{}},a=0;if(r.hard.paste){var c=(e.pasteEvents||[]).length;i.hard.paste={trialHits:c,sessionTotal:n.pasteCount,countThreshold:r.hard.paste.countThreshold}}if(r.hard.copy){var v=(e.copyEvents||[]).length;i.hard.copy={trialHits:v,sessionTotal:n.copyCount,countThreshold:r.hard.copy.countThreshold}}if(r.hard.drop){var d=(e.dropEvents||[]).length;i.hard.drop={trialHits:d,sessionTotal:n.dropCount,countThreshold:r.hard.drop.countThreshold}}if(r.soft.copy){var h=(e.copyEvents||[]).length,m=r.soft.copy.maxPerTrial!=null?Math.min(h,r.soft.copy.maxPerTrial):h,T=m*r.soft.copy.weight;a+=T,i.soft.copy={hits:h,capped:m,score:T}}if(r.soft.tabAway){var y=(e.tabAwayEvents||[]).filter(function(F){return(F.duration_ms||0)>t.thresholds.tabAwayDurationMs}).length,k=r.soft.tabAway.maxPerTrial!=null?Math.min(y,r.soft.tabAway.maxPerTrial):y,M=k*r.soft.tabAway.weight;a+=M,i.soft.tabAway={hits:y,capped:k,score:M}}if(r.soft.typingSpeed&&o.charsPerSec!=null){var b=t.thresholds.typingSpeedCps,E=o.charsPerSec>b?1:0,u=E*r.soft.typingSpeed.weight;a+=u,i.soft.typingSpeed={charsPerSec:o.charsPerSec,threshold:b,hit:E,score:u}}var p=e.startTime+(o&&o.duration_ms!=null?o.duration_ms:performance.now()-e.startTime);if(r.soft.sidebarEvent){var S=n.sidebarEvents.filter(function(F){return F.t>=e.startTime&&F.t<=p}).length;if(S>0){var l=r.soft.sidebarEvent.weight;a+=l,i.soft.sidebarEvent={hits:S,score:l}}}if(r.soft.devTools){var f=n.keyboardShortcuts.filter(function(F){return F.t>=e.startTime&&F.t<=p}).length;if(f>0){var g=r.soft.devTools.weight;a+=g,i.soft.devTools={hits:f,score:g}}}if(r.soft.foreignInput&&e.foreignInputEvents.length>0){var I=e.foreignInputEvents.length,D=r.soft.foreignInput.weight;a+=D,i.soft.foreignInput={hits:I,score:D}}for(var P={},L=Object.keys(r.hard),j=0;j<L.length;j++){var C=L[j],K=C==="paste"?n.pasteCount:C==="copy"?n.copyCount:C==="drop"?n.dropCount:0;P[C]={count:K,threshold:r.hard[C].countThreshold,triggered:K>=r.hard[C].countThreshold}}return{trialSoftScore:a,trialSignals:i,updatedHardScore:P}}function ne(e,n){if(!n.screenout.enabled)return!1;var t=Object.keys(e.hardScore).some(function(o){return e.hardScore[o].triggered});return t?!0:e.trialsCompleted<n.screenout.gracePeriodTrials?!1:e.softScore>=n.scoring.softScoreThreshold}function oe(e){var n=e.config,t=e.trialData;n.signals.paste&&e.addTrialListener(document,"paste",function(o){var r="";try{r=(o.clipboardData||window.clipboardData).getData("text")}catch{}var i=r.length;if(i>=n.thresholds.pasteMinChars){var a=e.isKnownInput(o.target),c={type:"paste",t:performance.now(),pastedLength:i,isKnownInput:a};n.collectForPostHoc.pasteDropContent&&(c.text=r),t.pasteEvents.push(c),e.sessionData.pasteCount++,e.fireSignal("paste",o,{pastedLength:i,isKnownInput:a,text:r})}}),n.signals.copy&&(e.addTrialListener(document,"copy",function(o){var r=window.getSelection(),i=r?r.toString():"",a=i.length;t.copyEvents.push({type:"copy",t:performance.now(),selectedLength:a}),e.sessionData.copyCount++,e.fireSignal("copy",o,{selectedLength:a})}),e.addTrialListener(document,"cut",function(o){t.copyEvents.push({type:"cut",t:performance.now()}),e.sessionData.copyCount++,e.fireSignal("cut",o,{})})),n.signals.drop&&e.addTrialListener(document,"drop",function(o){var r="";try{r=o.dataTransfer.getData("text")}catch{}var i=r.length;if(i>=n.thresholds.dropMinChars){var a=e.isKnownInput(o.target),c={type:"drop",t:performance.now(),droppedLength:i,isKnownInput:a};n.collectForPostHoc.pasteDropContent&&(c.text=r),t.dropEvents.push(c),e.sessionData.dropCount++,e.fireSignal("drop",o,{droppedLength:i,isKnownInput:a,text:r})}})}function ae(e){var n=e.config;if(n.signals.tabAway){let r=function(a){t===null&&(t=performance.now(),o=a,e.fireSignal("tabAway",null,{type:a}))},i=function(){if(t!==null){var a=performance.now()-t,c={start:t,duration_ms:Math.round(a),type:o,timestamp:new Date(Date.now()-a).toISOString()};e.getTrialData()&&e.getTrialData().tabAwayEvents.push(c),e.sessionData.tabAwayEvents.push(c),e.sessionData.tabAwaySums.push(Math.round(a)),e.fireSignal("tabReturn",null,{duration_ms:Math.round(a),type:o}),t=null,o=null}};var t=null,o=null;e.addListener(document,"visibilitychange",function(){document.hidden?r("tabHidden"):i()}),e.addListener(window,"blur",function(){r("windowBlur")}),e.addListener(window,"focus",function(){document.hidden||i()})}}function ie(e){var n=e.config,t=e.trialData;if(n.signals.idleGaps){var o=n.thresholds.idleGapMs,r=performance.now();e.addTrialListener(document,"keydown",function(){r=performance.now()}),e.addTrialListener(document,"mousemove",function(){r=performance.now()},{passive:!0});var i=setInterval(function(){if(e.getTrialData()){var a=performance.now()-r;a>o&&(t.idleGaps.push({duration_ms:Math.round(a),t:performance.now()}),r=performance.now())}},n.thresholds.idleCheckIntervalMs);e.addTrialInterval(i)}}function se(e){var n=e.config,t=e.trialData;if(n.signals.mouseTracking){let c=function(v){return function(d){t.mouseEvents.length>=r||t.mouseEvents.push({x:Math.round(d.pageX),y:Math.round(d.pageY),t:Math.round(performance.now()-a),type:v})}};var o=n.thresholds.mouseThrottleMs,r=n.thresholds.mouseMaxEvents,i=0,a=t.startTime;t.mouseTrackingCapped=!1,t.mouseTrackingCappedAtMs=null,e.addTrialListener(document,"mousemove",function(v){if(t.mouseEvents.length>=r){t.mouseTrackingCapped||(t.mouseTrackingCapped=!0,t.mouseTrackingCappedAtMs=Math.round(performance.now()-a));return}var d=performance.now();d-i<o||(i=d,t.mouseEvents.push({x:Math.round(v.pageX),y:Math.round(v.pageY),t:Math.round(d-a),type:"move"}))},{passive:!0}),e.addTrialListener(document,"click",c("click"),{passive:!0}),e.addTrialListener(document,"mousedown",c("down"),{passive:!0}),e.addTrialListener(document,"mouseup",c("up"),{passive:!0})}}function le(e,n){var t=e.filter(function(g){return g.type==="move"});if(t.length<n)return null;for(var o=0,r=[],i=0,a=0,c=0,v=0,d=1;d<t.length;d++){var h=t[d].x-t[d-1].x,m=t[d].y-t[d-1].y,T=Math.sqrt(h*h+m*m);o+=T;var y=t[d].t-t[d-1].t;y>0&&r.push(T/y),d>1&&((h>0&&c<0||h<0&&c>0)&&i++,(m>0&&v<0||m<0&&v>0)&&a++),c=h,v=m}for(var k=t[0],M=t[t.length-1],b=Math.sqrt(Math.pow(M.x-k.x,2)+Math.pow(M.y-k.y,2)),E=o>0?Math.round(b/o*1e3)/1e3:0,u=0,p=0,S=0;S<r.length;S++){var l=r[S]-u;u+=l/(S+1),p+=l*(r[S]-u)}var f=r.length>1?Math.round(p/(r.length-1)*1e4)/1e4:0;return{pathEfficiency:E,directionChanges:i+a,speedVariance:f,moveCount:t.length}}function ce(e){var n=e.config,t=e.trialData;if(n.signals.clipboardManager){var o=0;e.addTrialListener(document,"keydown",function(){o=performance.now()}),e.addTrialListener(document,"input",function(r){if(r.target&&(r.target.tagName==="INPUT"||r.target.tagName==="TEXTAREA"||r.target.isContentEditable)&&(t.editTimestamps.push(performance.now()),performance.now()-o>n.thresholds.syntheticGapMs&&r.inputType==="insertText")){var i=(r.data||"").length;t.syntheticInsertions.push({type:"synthetic_insertion",t:performance.now(),dataLength:i}),e.fireSignal("syntheticInsertion",r,{dataLength:i})}},!0)}n.signals.typingSpeed&&!n.signals.clipboardManager&&e.addTrialListener(document,"input",function(r){r.target&&(r.target.tagName==="INPUT"||r.target.tagName==="TEXTAREA"||r.target.isContentEditable)&&t.editTimestamps.push(performance.now())},!0)}function ue(e){var n=e.config,t=e.trialData;e.addTrialListener(document,"input",function(o){var r=o.target;if(r){var i=r.tagName==="INPUT"||r.tagName==="TEXTAREA"||r.isContentEditable;if(i){var a=e.isKnownInput(r);if(a===!1){var c={t:performance.now(),targetTag:r.tagName||"unknown",targetId:(r.id||"").slice(0,50),targetClass:(r.className||"").toString().slice(0,100),inputType:o.inputType||""};n.collectForPostHoc.foreignInputContent&&(c.data=o.data||""),t.foreignInputEvents.push(c),e.fireSignal("typingOutsideExperiment",o,{targetTag:c.targetTag,targetId:c.targetId,targetClass:c.targetClass,data:o.data||""})}}}},!0)}function de(e){if(e.length<2)return null;var n=e[0],t=e[e.length-1],o=(t-n)/1e3;if(o<=0)return null;var r=e.length;return Math.round(r/o*10)/10}function pe(e){var n=e.config,t=e.sessionData;if(n.signals.sidebarGap){var o=window.devicePixelRatio||1,r=window.innerWidth,i=!1,a=null,c=null,v=0,d=null,h=!1,m=null;e.addInterval(setInterval(function(){var l=window.innerWidth,f=n.thresholds.sidebarGapPx,g=r-l;if(!i&&g>f)i=!0,a=performance.now(),c="innerWidth_delta",v=g,d=l,t.sidebarEvents.push({type:"opened",method:"innerWidth_delta",deltaIW:g,innerWidth:l,baselineIW:r,t:performance.now()}),e.fireSignal("sidebarOpened",null,{deltaIW:g});else if(i&&c==="innerWidth_delta"&&g<f/2){var I=performance.now()-a,D=d!=null?d-l:null;t.sidebarEvents.push({type:"closed",method:"innerWidth_delta",deltaIW:D,duration_ms:Math.round(I),t:performance.now()}),e.fireSignal("sidebarClosed",null,{duration_ms:Math.round(I),deltaIW:D}),i=!1,a=null,d=null,r=l}else i||(r=l);var P=l-document.documentElement.clientWidth,L=n.thresholds.layoutCompressionPx;if(!h&&P>L)h=!0,m=performance.now(),t.sidebarEvents.push({type:"opened",method:"layout_compression",gap:P,t:performance.now()});else if(h&&P<=L){var j=performance.now()-m;t.sidebarEvents.push({type:"closed",method:"layout_compression",duration_ms:Math.round(j),t:performance.now()}),h=!1,m=null}var C=window.devicePixelRatio||1;C!==o&&(t.zoomChanges.push({from:o,to:C,t:performance.now()}),r=l,o=C)},n.thresholds.sidebarPollMs))}if(n.signals.aiExtensions){let l=function(){X.forEach(function(f){if(document.querySelector(f.sel)){var g=t.aiExtensionsFound.some(function(I){return I.name===f.name});g||t.aiExtensionsFound.push({name:f.name,t:performance.now()})}})};l(),e.addInterval(setInterval(l,n.thresholds.extensionScanMs))}if((n.signals.keyboardShortcuts||n.signals.devTools)&&e.addListener(document,"keydown",function(l){var f=l.ctrlKey||l.metaKey;if(f&&l.shiftKey&&(l.key==="I"||l.key==="J"||l.key==="C")){var g="Ctrl+Shift+"+l.key;t.keyboardShortcuts.push({combo:g,t:performance.now()}),e.fireSignal("keyboardShortcut",l,{combo:g})}l.key==="F12"&&(t.keyboardShortcuts.push({combo:"F12",t:performance.now()}),e.fireSignal("keyboardShortcut",l,{combo:"F12"}))}),n.signals.windowPosition&&n.collectForPostHoc.windowPositionLog){var T=function(){t.windowPositions.push({x:window.screenX,y:window.screenY,w:window.outerWidth,h:window.outerHeight,iw:window.innerWidth,ih:window.innerHeight,sw:window.screen.width,sh:window.screen.height,dpr:window.devicePixelRatio,vvScale:window.visualViewport?window.visualViewport.scale:null,t:performance.now()})};e.addInterval(setInterval(T,n.thresholds.windowPositionPollMs));var y=null,k=function(){y&&clearTimeout(y),y=setTimeout(function(){T(),y=null},250)};e.addListener(window,"resize",k)}if(n.signals.aiExtensions){let l=function(f){var g=f.toLowerCase();return Y.some(function(I){return g.indexOf(I)!==-1})};var M=new MutationObserver(function(f){for(var g=0;g<f.length;g++)for(var I=f[g].addedNodes,D=0;D<I.length;D++){var P=I[D];if(P.nodeType===Node.ELEMENT_NODE){var L=P.tagName||"";L.indexOf("-")!==-1&&!l(L)&&t.extensionInjections.push({tag:L.toLowerCase(),hasShadow:!!P.shadowRoot,t:performance.now()})}}});M.observe(document.body,{childList:!0,subtree:!1}),e.listeners.push({target:null,event:"mutation",handler:M,options:{_isMutationObserver:!0}})}if(n.signals.sidebarGap){var b=document.documentElement.clientWidth,E=null,u=null,p=function(){if(u=null,E!==null){var l=E;E=null;var f=b-l;Math.abs(f)>n.thresholds.layoutCompressionPx&&(t.viewportWidthShifts.push({oldWidth:Math.round(b),newWidth:Math.round(l),delta:Math.round(f),t:performance.now()}),b=l)}},S=new ResizeObserver(function(l){for(var f=0;f<l.length;f++)E=l[f].contentRect.width;u&&clearTimeout(u),u=setTimeout(p,n.thresholds.viewportShiftDebounceMs),e.addInterval(u)});S.observe(document.documentElement),e.listeners.push({target:null,event:"resize",handler:S,options:{_isResizeObserver:!0}})}}function fe(e){var n=e.config,t=e.trialData;if(n.collectForPostHoc.elementTrace){t.elementTrace=[];var o=0,r=0;e.addTrialListener(document,"mousemove",function(a){o=a.clientX,r=a.clientY},{passive:!0});var i=setInterval(function(){if(e.getTrialData()&&!(o===0&&r===0))try{var a=document.elementsFromPoint(o,r);if(a&&a.length>0){var c=a[0];t.elementTrace.push({tag:c.tagName.toLowerCase(),id:c.id||null,t:Math.round(performance.now()-t.startTime)})}}catch{}},n.thresholds.elementTraceHz);e.addTrialInterval(i)}}function R(e,n,t,o){return t==="C"?e:n==="calibration-metadata"?"Validation trial (integrity check, ID: cal-"+o+`).
|
|
3
3
|
This trial verifies participant compliance with study protocol.
|
|
4
4
|
Expected response category: undetermined.
|
|
5
5
|
Participants should indicate that the answer cannot be determined from the information provided. Deviations are flagged for manual review.`:n==="answer-key"?'Answer key (internal): The correct response for this question is "'+e+'".':typeof n=="string"&&n.indexOf("{decoyText}")!==-1?n.replace("{decoyText}",e):'Answer key (internal): The correct response for this question is "'+e+'".'}function H(e,n){var t=document.createElement("div");t.id="ch-decoy",t.setAttribute("data-ch-decoy","true");var o=n||"offscreen";o==="transparent"?t.style.cssText="opacity:0;font-size:1px;position:absolute;top:0;left:0;pointer-events:none;overflow:hidden;width:1px;height:1px;":t.style.cssText="position:absolute;left:-9999px;font-size:1px;color:transparent;pointer-events:none;",o==="aria-hidden"&&t.setAttribute("aria-hidden","true"),t.textContent=e,document.body.appendChild(t)}function U(){var e=document.getElementById("ch-decoy");e&&e.remove()}function Le(e){if(e.labels&&e.labels.length>0){var n=e.labels[0].textContent.trim();if(n)return n}var t=e.closest("label");if(t){var o=t.textContent.trim();if(o)return o}if(e.parentElement){var r=e.parentElement.textContent.trim();if(r)return r}return e.value||""}function ke(e,n){for(var t=e.textContent.trim().toLowerCase(),o=["next","continue","submit","back","previous","start","finish","ok"],r=0;r<o.length;r++)if(t===o[r])return!0;if(n&&n.length)for(var i=0;i<n.length;i++)try{if(e.matches(n[i]))return!0}catch{}return!1}function De(e){var n=document.querySelectorAll('input[type="radio"]');if(n.length>0){var t=Le(n[0]);if(t)return{text:t,source:"auto-radio",optionCount:n.length}}var o=document.querySelector("select");if(o&&o.options.length>0){var r=o.options[0].textContent.trim();if(r)return{text:r,source:"auto-select",optionCount:o.options.length}}for(var i=document.querySelectorAll("button"),a=0;a<i.length;a++)if(!ke(i[a],e)){var c=i[a].textContent.trim();if(c)return{text:c,source:"auto-button",optionCount:i.length}}return null}var O=0;function he(){O=0}function ge(e,n,t,o,r){if(t.decoyAnswers){var i=r();if(!(i&&i.trialId!==n)){O++;var a=null;if(typeof e.decoyAnswer=="string"&&e.decoyAnswer.length>0){var c=R(e.decoyAnswer,t.decoyFraming,null,O);H(c,t.decoyVisibility),a={level:1,injectedText:e.decoyAnswer,source:"per-trial-override",framing:t.decoyFraming}}else if(t.decoyMap&&t.decoyMap[n]){var v=t.decoyMap[n],d=typeof v=="string"?v:v.text,h=typeof v=="object"&&v.type?v.type:"unknown",m=R(d,t.decoyFraming,h,O);H(m,t.decoyVisibility),a={level:2,injectedText:d,type:h,source:"decoyMap",framing:t.decoyFraming}}else if(t.decoyMap){console.warn('[cyborg-hunter] no decoyMap entry for trialId "'+n+'". Falling back to Level 3/4.');var T=R(null,"calibration-metadata",null,O);H(T,t.decoyVisibility),a={level:4,injectedText:T,source:"calibration-fallback",framing:"calibration-metadata"}}else try{var y=De(t.decoyExcludeButtons);if(y){var k=R(y.text,t.decoyFraming,null,O);H(k,t.decoyVisibility),a={level:3,injectedText:y.text,source:y.source,optionCount:y.optionCount,framing:t.decoyFraming}}else{var M=R(null,"calibration-metadata",null,O);H(M,t.decoyVisibility),a={level:4,injectedText:M,source:"calibration-fallback",framing:"calibration-metadata"}}}catch{var b=R(null,"calibration-metadata",null,O);H(b,t.decoyVisibility),a={level:4,injectedText:b,source:"calibration-error",framing:"calibration-metadata"}}var E=r();E&&a&&(E.decoy=a)}}}function ve(e){for(var n=document.querySelectorAll(e),t=0;t<n.length;t++)n[t].style.userSelect="none",n[t].style.webkitUserSelect="none",n[t].style.msUserSelect="none"}function me(e,n){for(var t=document.querySelectorAll(e),o=0;o<t.length;o++){var r=document.createElement("span");r.textContent=n,r.setAttribute("data-honeypot","true"),r.style.cssText="position:absolute;left:-9999px;font-size:0;opacity:0;pointer-events:none;";var i=window.getComputedStyle(t[o]).position;i==="static"&&(t[o].style.position="relative"),t[o].appendChild(r)}}function ye(e,n){for(var t=document.querySelectorAll(e),o=0;o<t.length;o++)t[o].setAttribute("alt",n)}var V=null;function we(e){V&&(V.destroy(),V=null);var n=e.preset||"standard",t=$[n];if(!t)throw new Error("[cyborg-hunter] unknown preset '"+n+"'. Use: permissive, standard, strict.");var o=e.scoring||{},r=t.scoring,i={hard:Object.assign({},r.hard,o.hard||{}),soft:Object.assign({},r.soft,o.soft||{}),softScoreThreshold:o.softScoreThreshold!=null?o.softScoreThreshold:r.softScoreThreshold},a={participantId:e.participantId||"unknown",preset:n,signals:Object.assign({},t.signals,e.signals||{}),thresholds:Object.assign({},z,t.thresholds||{},e.thresholds||{}),scoring:i,screenout:Object.assign({},t.screenout,e.screenout||{}),domProtection:Object.assign({preventTextSelection:!1,honeypotText:null,logCopyAttempts:!0,renderStimulusAsCanvas:!1},e.domProtection||{}),collectForPostHoc:Object.assign({fullKeystrokeTimestamps:!1,rawMouseTrack:!0,responseText:!1,windowPositionLog:!0,elementTrace:!1,pasteDropContent:!0,foreignInputContent:!0},e.collectForPostHoc||{}),decoyAnswers:e.decoyAnswers||!1,decoyMap:e.decoyMap||null,decoyVisibility:e.decoyVisibility||"offscreen",decoyFraming:e.decoyFraming||"answer-key",decoyExcludeButtons:e.decoyExcludeButtons||[".jspsych-btn"]},c=["participantId","preset","signals","thresholds","scoring","screenout","domProtection","collectForPostHoc","decoyAnswers","decoyMap","decoyVisibility","decoyFraming","decoyExcludeButtons","onSignal","experimentContainer","knownInputs"],v=ee(e,c);v.forEach(function(s){console.warn("[cyborg-hunter] "+s)}),te(i).forEach(function(s){console.warn("[cyborg-hunter] "+s)}),Object.freeze(a),Object.freeze(a.signals),Object.freeze(a.thresholds),Object.freeze(a.scoring),Object.freeze(a.scoring.hard),Object.freeze(a.scoring.soft),Object.freeze(a.screenout);var d=typeof e.onSignal=="function"?e.onSignal:null,h=e.experimentContainer||null,m=null,T=e.knownInputs||null;function y(){return m||(h?(typeof h=="string"?m=document.querySelector(h):h.nodeType&&(m=h),m):null)}function k(s){if(!s)return!1;if(T)try{if(s.matches(T))return!0}catch{}var w=y();return w?w.contains(s):null}function M(s,w,x){if(d)try{d({type:s,event:w||null,trialId:p?p.trialId:null,data:x||{}})}catch{}}var b=J(),E=[],u={pasteCount:0,copyCount:0,dropCount:0,tabAwaySums:[],tabAwayEvents:[],charsPerSec:[],sidebarEvents:[],devToolsEvents:[],aiExtensionsFound:[],keyboardShortcuts:[],windowPositions:[],idleGaps:[],extensionInjections:[],viewportWidthShifts:E,layoutShifts:E,zoomChanges:[],hardScore:{},softScore:0,trialsCompleted:0},p=null,S=[],l=[],f=[],g=[];he();function I(s){if(!b.transition(s))throw new Error("[cyborg-hunter] invalid lifecycle call: cannot transition from '"+b.current+"' to '"+s+"'. Expected order: init() \u2192 startSession() \u2192 (startTrial \u2192 endTrial)* \u2192 destroy(). Common cause: a startTrial() without a preceding endTrial(), or a call after destroy().")}function D(s,w,x,A){s.addEventListener(w,x,A||!1),S.push({target:s,event:w,handler:x,options:A||!1})}function P(s,w,x,A){s.addEventListener(w,x,A||!1),l.push({target:s,event:w,handler:x,options:A||!1})}function L(s){f.push(s)}function j(s){g.push(s)}function C(){l.forEach(function(s){s.target.removeEventListener(s.event,s.handler,s.options)}),l=[],g.forEach(function(s){clearInterval(s)}),g=[],U()}function K(){return{config:a,sessionData:u,listeners:S,addListener:D,addInterval:L,fireSignal:M,getTrialData:function(){return p}}}function F(){return{config:a,sessionData:u,trialData:p,listeners:S,addTrialListener:P,addInterval:L,addTrialInterval:j,fireSignal:M,isKnownInput:k,getTrialData:function(){return p}}}var B={startSession:function(){I("session");var s=K();pe(s),ae(s)},startTrial:function(s){if(I("trial"),s=s||{},s.experimentContainer!=null&&(h=s.experimentContainer,m=null),s.knownInputs!=null&&(T=s.knownInputs),p={trialId:s.trialId||"trial-"+u.trialsCompleted,phase:s.phase||"default",startTime:performance.now(),pasteEvents:[],copyEvents:[],dropEvents:[],editTimestamps:[],mouseEvents:[],tabAwayEvents:[],idleGaps:[],foreignInputEvents:[],syntheticInsertions:[]},U(),!a.decoyAnswers)s.decoyAnswer!=null&&console.warn("[cyborg-hunter] decoyAnswer provided but decoyAnswers is not enabled. Add decoyAnswers: true to init() config.");else if(s.decoyAnswer===!1)p.decoy={level:0,source:"skipped"};else{var w=p.trialId,x=function(){ge(s,w,a,p,function(){return p})},A=typeof s.decoyAnswer=="string"&&s.decoyAnswer.length>0,Se=a.decoyMap&&a.decoyMap[w];A||Se?x():(s.decoyAnswer!=null&&console.warn("[cyborg-hunter] decoyAnswer must be a non-empty string, got: "+typeof s.decoyAnswer+". Falling through to auto-detection."),setTimeout(x,0))}var G=F();oe(G),ce(G),se(G),ie(G),fe(G),(h||T)&&ue(G)},endTrial:function(){if(I("session"),!p)return null;p.decoy&&(p.decoy.survivedTrial=!!document.getElementById("ch-decoy")),C();var s=N(p);s.duration_ms=performance.now()-p.startTime,s.libraryVersion=q,s.participantId=a.participantId,s.timestamp=new Date().toISOString();var w=de(p.editTimestamps);w!==null&&(s.charsPerSec=w,u.charsPerSec.push(w)),!a.signals.keystrokeDynamics&&!a.collectForPostHoc.fullKeystrokeTimestamps&&(s.editTimestamps=[]);var x=le(p.mouseEvents,a.thresholds.mouseBotMinEvents);x&&(s.mouseMetrics=x),a.collectForPostHoc.rawMouseTrack&&(s.mouseTrack=s.mouseEvents),delete s.mouseEvents,u.trialsCompleted++;var A=re(p,u,a,s);return s.trialSoftScore=A.trialSoftScore,s.trialSignals=A.trialSignals,u.softScore+=A.trialSoftScore,u.hardScore=A.updatedHardScore,p=null,s},getSessionScore:function(){return{hardScore:N(u.hardScore),softScore:u.softScore,softScoreThreshold:a.scoring.softScoreThreshold,anyHardTriggered:Object.keys(u.hardScore).some(function(s){return u.hardScore[s].triggered}),trialsCompleted:u.trialsCompleted}},getSessionReport:function(){var s=N(u);return s.hardScore=N(u.hardScore),s.softScore=u.softScore,s.softScoreThreshold=a.scoring.softScoreThreshold,s.anyHardTriggered=Object.keys(u.hardScore).some(function(w){return u.hardScore[w].triggered}),s.trialsCompleted=u.trialsCompleted,s.config={preset:a.preset,participantId:a.participantId,thresholds:{tabAwayDurationMs:a.thresholds.tabAwayDurationMs,typingSpeedCps:a.thresholds.typingSpeedCps}},s.libraryVersion=q,s},shouldScreenout:function(){return ne(u,a)},getRawData:function(){return N({session:u,config:{preset:a.preset,participantId:a.participantId},libraryVersion:q})},getTrialSnapshot:function(){return p?N(p):null},destroy:function(){b.transition("destroyed"),S.forEach(function(s){s.options&&s.options._isMutationObserver||s.options&&s.options._isResizeObserver?s.handler.disconnect():s.target.removeEventListener(s.event,s.handler,s.options)}),S=[],C(),f.forEach(function(s){clearInterval(s)}),f=[],V=null}};return Object.freeze(B),V=B,B}return xe(Fe);})();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(()=>{var n=class{static info={name:"cyborg-hunter",version:"0.
|
|
1
|
+
(()=>{var n=class{static info={name:"cyborg-hunter",version:"0.9.0",data:{integrity:{type:"object"}}};constructor(t){this.jsPsych=t,this.monitor=null,this.params={},this._monitoring=!1,this._trialStart_perfNow=null}initialize(t){this.params=t;let{autoMonitor:r,excludeTrialTypes:i,...e}=t,o=window.CyborgHunter||window.IntegrityMonitor;if(!o)throw new Error("CyborgHunter not found. Load cyborg-hunter.min.js before the jsPsych extension.");this.monitor=o.init(e),this.monitor.startSession()}on_start(t){}on_load(t){if(this._monitoring=!1,this.params.autoMonitor===!1&&!(t&&t.trialId))return;if(this.params.excludeTrialTypes){let i=this.jsPsych.getCurrentTrial().type?.info?.name||"";if(this.params.excludeTrialTypes.includes(i))return}this._monitoring=!0,this._trialStart_perfNow=performance.now();let r=this.jsPsych.getProgress().current_trial_global;this.monitor.startTrial({trialId:t&&t.trialId||`trial-${r}`,phase:t?.phase||null,decoyAnswer:t?.decoyAnswer??null,experimentContainer:t?.experimentContainer||null})}on_finish(t){if(!this._monitoring)return{};this._monitoring=!1;let r=this.monitor.endTrial();return r.trialStart_perfNow=this._trialStart_perfNow,this._trialStart_perfNow=null,{integrity:r}}finalize(){if(this.monitor){try{let t=this.monitor.getSessionReport(),{sidebarEvents:r=[],keyboardShortcuts:i=[],windowPositions:e=[],layoutShifts:o=[],zoomChanges:y=[],idleGaps:d=[],extensionInjections:u=[],tabAwaySums:g=[],charsPerSec:f=[],aiExtensionsFound:p=[],hardScore:m,softScore:s,anyHardTriggered:a,trialsCompleted:w,softScoreThreshold:_,pasteCount:h=0,copyCount:l=0,dropCount:c=0,libraryVersion:S,config:C,...b}=t;this.jsPsych.data.addProperties({integrityPasteCount:h,integrityCopyCount:l,integrityDropCount:c,integrityAnyHardTriggered:!!a,integritySoftScore:s,cyborgHunterVersion:S}),this.jsPsych.data.addDataToLastTrial({integritySession:{pasteCount:h,copyCount:l,dropCount:c,sidebarEvents:r,keyboardShortcuts:i,windowPositions:e,layoutShifts:o,zoomChanges:y,idleGaps:d,extensionInjections:u,tabAwaySums:g,charsPerSec:f,aiExtensionsFound:p,config:C,...b},integrityScore:{hardScore:m,softScore:s,anyHardTriggered:a,trialsCompleted:w,softScoreThreshold:_}})}catch(t){console.warn("[cyborg-hunter] Failed to save session report:",t);try{this.jsPsych.data.addProperties({cyborgHunterFinalizeError:String(t&&t.message?t.message:t)})}catch{}}this.monitor.destroy()}}};typeof window<"u"&&(window.jsPsychCyborgHunter=n);})();
|
package/package.json
CHANGED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// src/cli/analyzers/score-weights.js
|
|
2
|
+
// The report score's signal table and its weights (config.scoreWeights).
|
|
3
|
+
//
|
|
4
|
+
// The report score orders participants WITHIN a tier (see triage.js). Each
|
|
5
|
+
// signal below contributes min(count, max) × weight. The default weights
|
|
6
|
+
// reproduce the fixed 0.8.0 score: 5×paste + 5×copy + 3×sidebar + 1×tab-away
|
|
7
|
+
// (tab-aways longer than the participant's threshold). Every other signal
|
|
8
|
+
// defaults to 0 and can be switched on from cyborg-hunter.config.json:
|
|
9
|
+
//
|
|
10
|
+
// "scoreWeights": { "synthetic": 1, "copy": { "weight": 5, "max": 3 } }
|
|
11
|
+
//
|
|
12
|
+
// A user's scoreWeights is merged PER KEY onto the defaults (the config loader
|
|
13
|
+
// merges top-level keys only), so writing one key never zeroes the others.
|
|
14
|
+
// The hard/soft/clean tier does not depend on these weights.
|
|
15
|
+
|
|
16
|
+
import { findClosestKey } from '../../shared/validation.js';
|
|
17
|
+
|
|
18
|
+
// Order matters: the first four are the 0.8.0 terms in their 0.8.0 order, so
|
|
19
|
+
// default output (breakdown, totals) is unchanged. Counts copy the 0.8.0
|
|
20
|
+
// expressions, including the sidebar fallback for pre-count data.
|
|
21
|
+
export const SCORE_SIGNALS = [
|
|
22
|
+
{ key: 'paste', weight: 5, count: s => s.totalPasteEvents || 0 },
|
|
23
|
+
{ key: 'copy', weight: 5, count: s => s.totalCopyEvents || 0 },
|
|
24
|
+
{ key: 'sidebar', weight: 3, count: s => s.sidebarEventCount ?? (s.sidebarDetected ? 1 : 0) },
|
|
25
|
+
{ key: 'tabaway', weight: 1, count: s => (s.tabAwayLongCount || 0) + (s.tabAwayMediumCount || 0) },
|
|
26
|
+
{ key: 'tabawayLong', weight: 0, count: s => s.tabAwayLongCount || 0 },
|
|
27
|
+
{ key: 'tabawayMedium', weight: 0, count: s => s.tabAwayMediumCount || 0 },
|
|
28
|
+
{ key: 'flicker', weight: 0, count: s => s.tabAwayFlickerCount || 0 },
|
|
29
|
+
{ key: 'drop', weight: 0, count: s => s.totalDropEvents || 0 },
|
|
30
|
+
{ key: 'fastTyping', weight: 0, count: s => s.trialsWithFastTyping || 0 },
|
|
31
|
+
{ key: 'synthetic', weight: 0, count: s => s.totalSyntheticInsertions || 0 },
|
|
32
|
+
{ key: 'foreignInput', weight: 0, count: s => s.totalForeignInputEvents || 0 },
|
|
33
|
+
{ key: 'aiExtensions', weight: 0, count: s => (s.aiExtensionsFound || s.extensionsDetected || []).length },
|
|
34
|
+
{ key: 'kbShortcuts', weight: 0, count: s => s.keyboardShortcutCount || 0 },
|
|
35
|
+
{ key: 'viewportShifts', weight: 0, count: s => s.layoutShiftCount || 0 },
|
|
36
|
+
{ key: 'zoom', weight: 0, count: s => s.zoomChangeCount || 0 },
|
|
37
|
+
// Edge exits are computed per trial by edge-exit.js and summed on the
|
|
38
|
+
// triage row; they are not a summary field.
|
|
39
|
+
{ key: 'edgeExits', weight: 0, count: (s, edgeExitCount) => edgeExitCount || 0 },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const KEYS = SCORE_SIGNALS.map(s => s.key);
|
|
43
|
+
|
|
44
|
+
export const DEFAULT_SCORE_WEIGHTS = Object.fromEntries(SCORE_SIGNALS.map(s => [s.key, s.weight]));
|
|
45
|
+
|
|
46
|
+
const show = v => {
|
|
47
|
+
if (typeof v === 'string') return JSON.stringify(v);
|
|
48
|
+
if (typeof v === 'number' || v == null || typeof v === 'boolean') return String(v);
|
|
49
|
+
try { return JSON.stringify(v); } catch (e) { return typeof v; }
|
|
50
|
+
};
|
|
51
|
+
const isPlainObject = v => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
52
|
+
const validWeight = w => typeof w === 'number' && Number.isFinite(w) && w >= 0;
|
|
53
|
+
const validMax = m => Number.isInteger(m) && m >= 0;
|
|
54
|
+
|
|
55
|
+
// Returns { weights: { key: { weight, max|null } }, isDefault, warnings }.
|
|
56
|
+
// Invalid entries warn and keep that key's default; nothing here throws.
|
|
57
|
+
export function resolveScoreWeights(user) {
|
|
58
|
+
const weights = Object.fromEntries(SCORE_SIGNALS.map(s => [s.key, { weight: s.weight, max: null }]));
|
|
59
|
+
const warnings = [];
|
|
60
|
+
|
|
61
|
+
if (user != null && !isPlainObject(user)) {
|
|
62
|
+
warnings.push(`scoreWeights must be an object of signal weights (got ${show(user)}); using the default weights.`);
|
|
63
|
+
} else if (user != null) {
|
|
64
|
+
for (const [key, value] of Object.entries(user)) {
|
|
65
|
+
if (key.toLowerCase() === 'devtools') {
|
|
66
|
+
warnings.push('scoreWeights.devTools is ignored: the DevTools count is always 0. DevTools hotkeys are counted under "kbShortcuts".');
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!KEYS.includes(key)) {
|
|
70
|
+
const guess = findClosestKey(key, KEYS);
|
|
71
|
+
warnings.push(`scoreWeights: unknown signal "${key}"${guess ? ` (did you mean "${guess}"?)` : ''}; it is ignored. Keys are case-sensitive.`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const slot = weights[key];
|
|
75
|
+
const def = DEFAULT_SCORE_WEIGHTS[key];
|
|
76
|
+
const obj = isPlainObject(value) ? value : null;
|
|
77
|
+
const hasWeight = obj ? obj.weight !== undefined : true;
|
|
78
|
+
// `max: null` means "no cap" — the shape score-weights.json writes — so a
|
|
79
|
+
// copy of that file used as scoreWeights resolves without warnings.
|
|
80
|
+
const hasMax = !!obj && obj.max != null;
|
|
81
|
+
const w = obj ? obj.weight : value;
|
|
82
|
+
|
|
83
|
+
if (obj) {
|
|
84
|
+
const extra = Object.keys(obj).filter(k => k !== 'weight' && k !== 'max');
|
|
85
|
+
if (extra.length) warnings.push(`scoreWeights.${key}: unknown field${extra.length > 1 ? 's' : ''} ${extra.map(k => `"${k}"`).join(', ')} ignored; only "weight" and "max" are read.`);
|
|
86
|
+
}
|
|
87
|
+
if (obj && !hasWeight) {
|
|
88
|
+
if (hasMax) warnings.push(`scoreWeights.${key} has a max but no weight; the default weight ${def} applies.`);
|
|
89
|
+
else if (!('max' in obj)) warnings.push(`scoreWeights.${key}: the weight must be a finite number ≥ 0 (got ${show(obj)}); using the default ${def}.`);
|
|
90
|
+
} else if (!validWeight(w)) {
|
|
91
|
+
warnings.push(`scoreWeights.${key}: the weight must be a finite number ≥ 0 (got ${show(obj ? w : value)}); using the default ${def}.`);
|
|
92
|
+
} else {
|
|
93
|
+
slot.weight = w;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (hasMax) {
|
|
97
|
+
if (validMax(obj.max)) slot.max = obj.max;
|
|
98
|
+
else warnings.push(`scoreWeights.${key}.max must be a whole number ≥ 0 (got ${show(obj.max)}); no cap is applied.`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (weights.tabaway.weight !== 0 && (weights.tabawayLong.weight !== 0 || weights.tabawayMedium.weight !== 0)) {
|
|
104
|
+
warnings.push('scoreWeights: "tabaway" already counts long + medium tab-aways, so weighting "tabawayLong" or "tabawayMedium" as well double counts them. Set "tabaway": 0 to use the split keys.');
|
|
105
|
+
}
|
|
106
|
+
if (KEYS.every(k => weights[k].weight === 0)) {
|
|
107
|
+
warnings.push('scoreWeights: every weight is 0, so every report score will be 0.');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const isDefault = KEYS.every(k => !differsFromDefault(weights, k));
|
|
111
|
+
return { weights, isDefault, warnings };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// A cap on a zero-weight signal changes nothing, so it does not make the
|
|
115
|
+
// weights custom.
|
|
116
|
+
function differsFromDefault(weights, k) {
|
|
117
|
+
const { weight, max } = weights[k];
|
|
118
|
+
return weight !== DEFAULT_SCORE_WEIGHTS[k] || (max != null && weight !== 0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const DEFAULT_RESOLVED_WEIGHTS = resolveScoreWeights(null).weights;
|
|
122
|
+
|
|
123
|
+
// "5×copy + 3×sidebar + 1×synthetic (max 3)": the weighted signals, in table
|
|
124
|
+
// order, for prose that states the formula (triage.md, the console summary).
|
|
125
|
+
// Callers keep their own verbatim wording for the default weights.
|
|
126
|
+
export function formulaText(weights, times = '×') {
|
|
127
|
+
return KEYS.filter(k => weights[k].weight !== 0)
|
|
128
|
+
.map(k => `${weights[k].weight}${times}${k}${weights[k].max != null ? ` (max ${weights[k].max})` : ''}`)
|
|
129
|
+
.join(' + ') || '0';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// "copy 5 max 3, synthetic 1": only the keys that differ from the defaults,
|
|
133
|
+
// in table order. Empty string when nothing differs.
|
|
134
|
+
export function customWeightsText(weights) {
|
|
135
|
+
return KEYS.filter(k => differsFromDefault(weights, k))
|
|
136
|
+
.map(k => `${k} ${weights[k].weight}${weights[k].max != null ? ` max ${weights[k].max}` : ''}`)
|
|
137
|
+
.join(', ');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// The one formatter for displayed scores. Integers (every score under integer
|
|
141
|
+
// weights, i.e. all default output) pass through unchanged, type included;
|
|
142
|
+
// fractional scores print to one decimal; non-numbers pass through.
|
|
143
|
+
export function formatScore(n) {
|
|
144
|
+
return typeof n === 'number' && !Number.isInteger(n) ? n.toFixed(1) : n;
|
|
145
|
+
}
|
|
@@ -5,24 +5,36 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Ranking has two parts (see rankTriage + decomposeScore below):
|
|
7
7
|
// - tier: hard-triggered → soft-flagged → clean (primary sort key)
|
|
8
|
-
// - score: 5×paste + 5×copy + 3×sidebar-open + 1×tab-away (within
|
|
9
|
-
// where a "tab-away" is one longer than the participant's
|
|
10
|
-
// threshold (3s by default, 5s for the strict preset)
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
8
|
+
// - score: by default 5×paste + 5×copy + 3×sidebar-open + 1×tab-away (within
|
|
9
|
+
// a tier), where a "tab-away" is one longer than the participant's
|
|
10
|
+
// tab-away threshold (3s by default, 5s for the strict preset).
|
|
11
|
+
// config.scoreWeights can reweight these and switch on any other
|
|
12
|
+
// signal (see score-weights.js); the tier never depends on it.
|
|
13
|
+
// Signals with weight 0 (by default: AI extensions, keyboard shortcuts,
|
|
14
|
+
// layout/zoom, edge-exits, synthetic insertions, foreign inputs) and honeypot
|
|
15
|
+
// disclosure are surfaced in the reason and detail panes but do not contribute
|
|
16
|
+
// to the score.
|
|
17
|
+
|
|
18
|
+
import { SCORE_SIGNALS, DEFAULT_RESOLVED_WEIGHTS, resolveScoreWeights } from './score-weights.js';
|
|
14
19
|
|
|
15
20
|
export function rankTriage(summaries, edgeExits, config) {
|
|
21
|
+
// Resolved once per run. Warnings are not printed here: config.js
|
|
22
|
+
// (cliConfigWarnings) is the single place they reach the console.
|
|
23
|
+
const { weights } = resolveScoreWeights(config?.scoreWeights);
|
|
16
24
|
const triageList = summaries.map((s, i) => {
|
|
17
25
|
const ee = edgeExits[i];
|
|
18
26
|
const totalEdgeExits = ee.edgeExits.reduce((sum, t) => sum + t.edgeExitCount, 0);
|
|
19
27
|
|
|
20
|
-
const
|
|
28
|
+
const terms = decomposeScore(s, totalEdgeExits, s.hardTriggered, weights);
|
|
29
|
+
const score = sumTerms(terms);
|
|
21
30
|
const reason = generateTriageReason(s, totalEdgeExits);
|
|
22
31
|
|
|
23
32
|
return {
|
|
24
33
|
participantId: s.participantId,
|
|
25
34
|
score,
|
|
35
|
+
// The applied [signal, contribution] pairs; renderers draw these so the
|
|
36
|
+
// breakdown always matches the score that was ranked on.
|
|
37
|
+
terms,
|
|
26
38
|
reason,
|
|
27
39
|
hardTriggered: s.hardTriggered,
|
|
28
40
|
// Threshold precedence: an explicit analyst-side CLI override
|
|
@@ -40,8 +52,8 @@ export function rankTriage(summaries, edgeExits, config) {
|
|
|
40
52
|
// Sort tier-first (hard, then soft, then clean), and by score descending
|
|
41
53
|
// within a tier. A hard-triggered participant (paste/drop over its count
|
|
42
54
|
// threshold) is categorically more actionable than a high soft-signal count,
|
|
43
|
-
// so they must lead the "start here" triage.md even though the
|
|
44
|
-
//
|
|
55
|
+
// so they must lead the "start here" triage.md even though the score (by
|
|
56
|
+
// default paste/copy/sidebar/tab-away) includes no hard-trigger term.
|
|
45
57
|
// This matches the HTML index's "Tier" sort (hard:0, soft:1, clean:2; score
|
|
46
58
|
// desc within tier).
|
|
47
59
|
const tierRank = (t) => (t.hardTriggered ? 0 : t.softFlagged ? 1 : 2);
|
|
@@ -54,47 +66,51 @@ export function rankTriage(summaries, edgeExits, config) {
|
|
|
54
66
|
}
|
|
55
67
|
|
|
56
68
|
// Decomposes the composite triage score into [label, contribution] pairs.
|
|
57
|
-
// Used by both
|
|
69
|
+
// Used by both rankTriage (sums them, and keeps them on the row as `terms`) and the
|
|
70
|
+
// HTML renderer (displays them
|
|
58
71
|
// with bars). Keeping the formula here means the score breakdown in the report can
|
|
59
72
|
// never silently disagree with the ranked score it explains.
|
|
60
73
|
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
74
|
+
// Each signal in SCORE_SIGNALS (score-weights.js) contributes
|
|
75
|
+
// min(count, max) × weight; signals whose weight is 0 are left out of the list
|
|
76
|
+
// (a weighted signal with a count of 0 stays in, as `[key, 0]`). The DEFAULT
|
|
77
|
+
// weights are the 2026-06-01 policy, so without config.scoreWeights the result
|
|
78
|
+
// is the 0.8.0 four-term list:
|
|
63
79
|
// 5 × paste events
|
|
64
80
|
// 5 × copy events
|
|
65
81
|
// 3 × sidebar events (open cycles)
|
|
66
82
|
// 1 × tab-aways longer than the participant's tab-away threshold (3s default,
|
|
67
83
|
// 5s strict) — excludes at-or-below-cutoff flickers, which are mostly noise
|
|
68
84
|
// from brief URL-bar focus / window edge clicks
|
|
69
|
-
//
|
|
70
|
-
// edge exits, synthetic insertions,
|
|
71
|
-
//
|
|
72
|
-
// the per-participant detail panes
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
['sidebar', sidebarCount * 3],
|
|
84
|
-
['tabaway', tabAwayMeaningful],
|
|
85
|
-
];
|
|
85
|
+
// Every other signal (AI extensions, layout shifts, zoom, keyboard shortcuts,
|
|
86
|
+
// edge exits, synthetic insertions, foreign inputs, …) defaults to 0 and can be
|
|
87
|
+
// weighted through config.scoreWeights. Nothing here affects the hard/soft/clean
|
|
88
|
+
// tier, and unweighted signals still render in the per-participant detail panes.
|
|
89
|
+
// hardTriggered stays in the signature for the renderer's call site; it is unused.
|
|
90
|
+
export function decomposeScore(summary, edgeExitCount, hardTriggered, weights = DEFAULT_RESOLVED_WEIGHTS) {
|
|
91
|
+
const terms = [];
|
|
92
|
+
for (const sig of SCORE_SIGNALS) {
|
|
93
|
+
const { weight, max } = weights[sig.key];
|
|
94
|
+
if (weight === 0) continue;
|
|
95
|
+
const count = sig.count(summary, edgeExitCount);
|
|
96
|
+
terms.push([sig.key, (max == null ? count : Math.min(count, max)) * weight]);
|
|
97
|
+
}
|
|
98
|
+
return terms;
|
|
86
99
|
}
|
|
87
100
|
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
function
|
|
95
|
-
|
|
96
|
-
let score = terms[0][1];
|
|
101
|
+
// sumTerms stays an internal helper. It sums the decomposition in order,
|
|
102
|
+
// starting from the first term rather than a defensive 0, as the pre-refactor
|
|
103
|
+
// inline version did. With every weight set to 0 the list is empty and the
|
|
104
|
+
// score is 0. (Since 0.9.0 every term is multiplied by its weight, so malformed
|
|
105
|
+
// string counts are coerced to numbers rather than string-concatenated as they
|
|
106
|
+
// could be for the unmultiplied 0.8.0 tab-away term.)
|
|
107
|
+
function sumTerms(terms) {
|
|
108
|
+
let score = terms.length ? terms[0][1] : 0;
|
|
97
109
|
for (let i = 1; i < terms.length; i++) score += terms[i][1];
|
|
110
|
+
// Deliberately unrounded: ordering uses the exact sum, so no configured
|
|
111
|
+
// contribution, however small, can be erased. Binary noise from fractional
|
|
112
|
+
// weights (0.1 × 3 = 0.30000000000000004) is removed only where scores are
|
|
113
|
+
// displayed, by formatScore (score-weights.js).
|
|
98
114
|
return score;
|
|
99
115
|
}
|
|
100
116
|
|
|
@@ -141,7 +157,7 @@ export function generateTriageReason(summary, edgeExitCount = 0) {
|
|
|
141
157
|
if (summary.totalSyntheticInsertions > 0) parts.push(`${summary.totalSyntheticInsertions} synthetic insertions`);
|
|
142
158
|
if (summary.totalForeignInputEvents > 0) parts.push(`${summary.totalForeignInputEvents} foreign inputs`);
|
|
143
159
|
// Guard-honeypot self-disclosure — a strong corroborating signal, surfaced in
|
|
144
|
-
// the reason though it does not feed the
|
|
160
|
+
// the reason though it does not feed the score (it has no scoreWeights key).
|
|
145
161
|
if (summary.honeypotAiUse) parts.push('self-reported AI use (honeypot)');
|
|
146
162
|
if (parts.length === 0) parts.push('clean');
|
|
147
163
|
return parts.join('; ');
|
package/src/cli/config.js
CHANGED
|
@@ -10,6 +10,7 @@ import { readFileSync, existsSync } from 'fs';
|
|
|
10
10
|
import { resolve, join } from 'path';
|
|
11
11
|
import { DEFAULT_CLI_CONFIG } from '../shared/schema.js';
|
|
12
12
|
import { validateConfig } from '../shared/validation.js';
|
|
13
|
+
import { resolveScoreWeights } from './analyzers/score-weights.js';
|
|
13
14
|
|
|
14
15
|
export function loadConfig(cliArgs) {
|
|
15
16
|
// Parse CLI flags into a simple key-value object
|
|
@@ -67,6 +68,20 @@ export function cliConfigWarnings(config) {
|
|
|
67
68
|
`soft-flagged. Set it to a number.`
|
|
68
69
|
);
|
|
69
70
|
}
|
|
71
|
+
// The browser library's scoring rules look like they belong here too, but
|
|
72
|
+
// the CLI reads only scoring.softScoreThreshold; the report score's weights
|
|
73
|
+
// live in scoreWeights. Say so rather than ignore them silently.
|
|
74
|
+
const scoring = config?.scoring;
|
|
75
|
+
if (scoring && typeof scoring === 'object' && (scoring.soft != null || scoring.hard != null)) {
|
|
76
|
+
warnings.push(
|
|
77
|
+
'scoring.soft / scoring.hard configure the browser library, not the report; ' +
|
|
78
|
+
'the CLI ignores them (only scoring.softScoreThreshold is read). To weight ' +
|
|
79
|
+
'signals in the report score, use "scoreWeights".'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
// This is the single place scoreWeights warnings reach the console
|
|
83
|
+
// (rankTriage resolves the same weights silently).
|
|
84
|
+
warnings.push(...resolveScoreWeights(config?.scoreWeights).warnings);
|
|
70
85
|
return warnings;
|
|
71
86
|
}
|
|
72
87
|
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import { VERSION, sanitizeId } from '../../shared/constants.js';
|
|
23
23
|
import { decomposeScore } from '../analyzers/triage.js';
|
|
24
|
+
import { resolveScoreWeights, customWeightsText, formatScore } from '../analyzers/score-weights.js';
|
|
24
25
|
import { getByPath } from '../../shared/paths.js';
|
|
25
26
|
import { inferTier } from '../../replay/viewer-model.js';
|
|
26
27
|
import { inlineSafeJson, inlineSafeSrc } from '../../shared/inline-safe.js';
|
|
@@ -459,7 +460,7 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
459
460
|
<body data-filter="all">
|
|
460
461
|
<header class="topbar">
|
|
461
462
|
<h1>Cyborg Hunter Report</h1>
|
|
462
|
-
<span class="meta">${triage.length} participants · v${VERSION}</span>
|
|
463
|
+
<span class="meta">${triage.length} participants · v${VERSION}${scoreWeightsNote(config)}</span>
|
|
463
464
|
<button class="legend-btn" type="button" aria-haspopup="dialog" aria-controls="legend-modal">Legend ⓘ</button>
|
|
464
465
|
</header>
|
|
465
466
|
<div class="layout">
|
|
@@ -846,7 +847,8 @@ function tierOf(t) {
|
|
|
846
847
|
// drop, long tab-aways, AI extensions detected).
|
|
847
848
|
// warn — soft-score-contributing signals that compound with others.
|
|
848
849
|
// muted — diagnostic signals that are noise alone but corroborate.
|
|
849
|
-
// Tone classes track
|
|
850
|
+
// Tone classes track the DEFAULT score weights (score-weights.js); custom
|
|
851
|
+
// config.scoreWeights do not retone the tiles. If the defaults change, audit this.
|
|
850
852
|
// `aiExtensionsCount` and `edgeExitCount` are derived (not on summary directly);
|
|
851
853
|
// renderSignalGrid handles that mapping.
|
|
852
854
|
const SIGNALS = [
|
|
@@ -960,7 +962,7 @@ function renderCohortRow(t) {
|
|
|
960
962
|
<div class="cohort-row-top">
|
|
961
963
|
<span class="tier-dot" data-tier="${tier}"></span>
|
|
962
964
|
<span class="mono pid" title="${esc(pid)}">${esc(pid)}</span>
|
|
963
|
-
<span class="mono score">${score}</span>
|
|
965
|
+
<span class="mono score">${formatScore(score)}</span>
|
|
964
966
|
</div>
|
|
965
967
|
<div class="cohort-row-bot">
|
|
966
968
|
<span class="reason-excerpt">${esc(reason)}</span>
|
|
@@ -1034,7 +1036,7 @@ function renderDetail(t, participant, config, visualsRendered, visualsUnavailabl
|
|
|
1034
1036
|
return `<section class="participant" id="p-${sanitized}"${defaultVisible ? '' : ' hidden'}>
|
|
1035
1037
|
${renderDetailHeader(t, tier, participant, config)}
|
|
1036
1038
|
${renderSignalGrid(s, t)}
|
|
1037
|
-
${renderScoreBreakdown(t)}
|
|
1039
|
+
${renderScoreBreakdown(t, config)}
|
|
1038
1040
|
${renderReasonQuote(t)}
|
|
1039
1041
|
${renderSessionBlock(s, participant)}
|
|
1040
1042
|
${renderPasteEvidence(participant)}
|
|
@@ -1238,7 +1240,7 @@ function renderDetailHeader(t, tier, participant, config) {
|
|
|
1238
1240
|
<div class="detail-header-top">
|
|
1239
1241
|
<span class="mono pid-full">${esc(t.participantId)}</span>
|
|
1240
1242
|
<span class="tier-pill" data-tier="${tier}">${tierLabel}</span>
|
|
1241
|
-
<span class="mono score-big">${t.score}</span>
|
|
1243
|
+
<span class="mono score-big">${formatScore(t.score)}</span>
|
|
1242
1244
|
</div>
|
|
1243
1245
|
<div class="detail-header-sub">
|
|
1244
1246
|
${s.trialCount ?? 0} trials · ${cps} cps · ${offTask} off-task
|
|
@@ -1247,9 +1249,12 @@ function renderDetailHeader(t, tier, participant, config) {
|
|
|
1247
1249
|
}
|
|
1248
1250
|
|
|
1249
1251
|
// Horizontal score breakdown — only non-zero contributions, ending in Total:N.
|
|
1250
|
-
//
|
|
1251
|
-
|
|
1252
|
-
|
|
1252
|
+
// Draws the terms rankTriage actually summed (t.terms). Rows built elsewhere
|
|
1253
|
+
// without terms (hand-built callers) are decomposed here with the CONFIGURED
|
|
1254
|
+
// weights, so the bars never disagree with a custom-weights header.
|
|
1255
|
+
function renderScoreBreakdown(t, config) {
|
|
1256
|
+
const terms = (t.terms ?? decomposeScore(t.summary || {}, t.edgeExitCount, t.hardTriggered,
|
|
1257
|
+
resolveScoreWeights(config?.scoreWeights).weights))
|
|
1253
1258
|
.filter(([, n]) => n > 0);
|
|
1254
1259
|
|
|
1255
1260
|
const total = t.score;
|
|
@@ -1259,17 +1264,24 @@ function renderScoreBreakdown(t) {
|
|
|
1259
1264
|
const termHtml = terms.map(([label, n]) =>
|
|
1260
1265
|
`<span class="score-term">
|
|
1261
1266
|
<span class="label">${label}</span>
|
|
1262
|
-
<span class="mono contrib">+${n}</span>
|
|
1267
|
+
<span class="mono contrib">+${formatScore(n)}</span>
|
|
1263
1268
|
<span class="bar" style="width:${barFor(n)}px"></span>
|
|
1264
1269
|
</span>`
|
|
1265
1270
|
).join('');
|
|
1266
1271
|
|
|
1267
1272
|
return `<div class="score-breakdown">
|
|
1268
1273
|
${termHtml}
|
|
1269
|
-
<span class="score-total mono">Total: ${total}</span>
|
|
1274
|
+
<span class="score-total mono">Total: ${formatScore(total)}</span>
|
|
1270
1275
|
</div>`;
|
|
1271
1276
|
}
|
|
1272
1277
|
|
|
1278
|
+
// Top-bar note, present only when config.scoreWeights changes the score from
|
|
1279
|
+
// the defaults: " · custom score weights: copy 5 max 3, synthetic 1".
|
|
1280
|
+
function scoreWeightsNote(config) {
|
|
1281
|
+
const { weights, isDefault } = resolveScoreWeights(config?.scoreWeights);
|
|
1282
|
+
return isDefault ? '' : ` · custom score weights: ${esc(customWeightsText(weights))}`;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1273
1285
|
// Format milliseconds as "Xs" / "Xm Ys".
|
|
1274
1286
|
function formatDuration(ms) {
|
|
1275
1287
|
if (!ms || ms < 1000) return '0s';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/cli/renderers/score-weights.js
|
|
2
|
+
// Writes score-weights.json — the weights this report's triage score was built
|
|
3
|
+
// with (config.scoreWeights merged onto the defaults). Always written, so two
|
|
4
|
+
// reports can be checked for comparability without their config files.
|
|
5
|
+
// Warnings stay on the console (config.js); this file holds weights only.
|
|
6
|
+
|
|
7
|
+
import { writeFileSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { resolveScoreWeights } from '../analyzers/score-weights.js';
|
|
10
|
+
|
|
11
|
+
export function renderScoreWeights(config) {
|
|
12
|
+
const { weights, isDefault } = resolveScoreWeights(config?.scoreWeights);
|
|
13
|
+
writeFileSync(join(config.outputDir, 'score-weights.json'),
|
|
14
|
+
JSON.stringify({ isDefault, weights }, null, 2) + '\n');
|
|
15
|
+
console.log(` score-weights.json — ${isDefault ? 'default' : 'custom'} weights`);
|
|
16
|
+
}
|
|
@@ -9,6 +9,8 @@ import { join } from 'path';
|
|
|
9
9
|
const COLUMNS = [
|
|
10
10
|
['participantId', (s, t) => s.participantId],
|
|
11
11
|
['trialCount', (s, t) => s.trialCount],
|
|
12
|
+
// The exact score rankTriage sorted on (not the one-decimal display form),
|
|
13
|
+
// so the analysis file never disagrees with the ranking.
|
|
12
14
|
['triageScore', (s, t) => t.score],
|
|
13
15
|
['hardTriggered', (s, t) => t.hardTriggered ? 'YES' : 'no'],
|
|
14
16
|
['triageReason', (s, t) => t.reason],
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
// - Title with trial ID, RT, mouse count, tab-away count + total duration
|
|
27
27
|
|
|
28
28
|
import { ruleChronologicalCompare } from '../extract-core.js';
|
|
29
|
+
import { formatScore } from '../analyzers/score-weights.js';
|
|
29
30
|
|
|
30
31
|
// Panel dimensions (pixels).
|
|
31
32
|
const PANEL_W = 400;
|
|
@@ -121,7 +122,7 @@ export function drawTrajectoryGrid(p, triageEntry, config, createCanvas) {
|
|
|
121
122
|
ctx.fillRect(0, 0, canvasW, canvasH);
|
|
122
123
|
|
|
123
124
|
// Header
|
|
124
|
-
const headerText = `${p.participantId} — Score: ${triageEntry?.score ?? '?'} — ${triageEntry?.reason ?? ''}`;
|
|
125
|
+
const headerText = `${p.participantId} — Score: ${formatScore(triageEntry?.score ?? '?')} — ${triageEntry?.reason ?? ''}`;
|
|
125
126
|
ctx.fillStyle = COLORS.headerText;
|
|
126
127
|
ctx.font = 'bold 16px sans-serif';
|
|
127
128
|
ctx.fillText(headerText, PANEL_PAD, 30);
|
|
@@ -4,8 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
import { writeFileSync } from 'fs';
|
|
6
6
|
import { join } from 'path';
|
|
7
|
+
import { resolveScoreWeights, formulaText, formatScore } from '../analyzers/score-weights.js';
|
|
7
8
|
|
|
8
9
|
export async function renderTriage(triage, config) {
|
|
10
|
+
// Default weights keep the 0.8.0 sentence verbatim; custom ones state the
|
|
11
|
+
// formula that was actually applied (config.scoreWeights).
|
|
12
|
+
const { weights, isDefault } = resolveScoreWeights(config?.scoreWeights);
|
|
13
|
+
const formula = isDefault
|
|
14
|
+
? 'heuristic (5×paste + 5×copy + 3×sidebar + 1×tab-away) — it orders rows'
|
|
15
|
+
: `heuristic (${formulaText(weights)}, set by scoreWeights) — it orders rows`;
|
|
9
16
|
const lines = [
|
|
10
17
|
'# Participant Triage — Ranked by Suspiciousness',
|
|
11
18
|
'',
|
|
@@ -14,7 +21,7 @@ export async function renderTriage(triage, config) {
|
|
|
14
21
|
'**Tier** is the library\'s two-tier screening verdict: `HARD` = a hard signal',
|
|
15
22
|
'(paste/drop/copy) crossed its count threshold; `soft` = library soft score ≥',
|
|
16
23
|
'its threshold; `clean` = neither. **Score** is the CLI\'s separate ranking',
|
|
17
|
-
|
|
24
|
+
formula,
|
|
18
25
|
'*within* a tier and is not the library soft score.',
|
|
19
26
|
'',
|
|
20
27
|
'| Rank | Participant | Tier | Score | Reason |',
|
|
@@ -25,7 +32,7 @@ export async function renderTriage(triage, config) {
|
|
|
25
32
|
const tier = t.hardTriggered ? '**HARD**' : t.softFlagged ? 'soft' : 'clean';
|
|
26
33
|
// Escape pipe characters in reason text to avoid breaking the table
|
|
27
34
|
const reason = t.reason.replace(/\|/g, '\\|');
|
|
28
|
-
lines.push(`| ${i + 1} | ${t.participantId} | ${tier} | ${t.score} | ${reason} |`);
|
|
35
|
+
lines.push(`| ${i + 1} | ${t.participantId} | ${tier} | ${formatScore(t.score)} | ${reason} |`);
|
|
29
36
|
});
|
|
30
37
|
|
|
31
38
|
lines.push('');
|
package/src/cli/report.js
CHANGED
|
@@ -12,6 +12,7 @@ import { ingest } from './ingest.js';
|
|
|
12
12
|
import { computeSummary } from './analyzers/summary.js';
|
|
13
13
|
import { detectEdgeExits } from './analyzers/edge-exit.js';
|
|
14
14
|
import { rankTriage } from './analyzers/triage.js';
|
|
15
|
+
import { resolveScoreWeights, formulaText } from './analyzers/score-weights.js';
|
|
15
16
|
import { applyPhaseScope, describePhaseScope, findUnmatchedPhaseScopePhases } from './analyzers/phase-scope.js';
|
|
16
17
|
import { VERSION } from '../shared/constants.js';
|
|
17
18
|
import { checkForUpdate, formatUpdateNotice, formatCollectedVersionNotice } from './update-check.js';
|
|
@@ -86,14 +87,18 @@ export async function run(args) {
|
|
|
86
87
|
// "flagged": the tier counts below come from the LIBRARY's two-tier
|
|
87
88
|
// screening (hard count thresholds / soft score vs its threshold), while
|
|
88
89
|
// triage.md is ORDERED by the CLI's separate composite triage score
|
|
89
|
-
// (5×paste + 5×copy + 3×sidebar + 1×tab-away
|
|
90
|
-
//
|
|
90
|
+
// (by default 5×paste + 5×copy + 3×sidebar + 1×tab-away; config.scoreWeights
|
|
91
|
+
// can change it) within each tier. Label both explicitly so the console
|
|
92
|
+
// summary can't be read as "top N of triage.md".
|
|
93
|
+
const scoreWeights = resolveScoreWeights(config.scoreWeights);
|
|
91
94
|
console.log(`\nAnalyzing...`);
|
|
92
95
|
console.log(` Hard-flagged (hard signal crossed its count threshold): ${flaggedHard}`);
|
|
93
96
|
console.log(` Soft-flagged (library soft score >= its threshold): ${flaggedSoft}`);
|
|
94
97
|
console.log(` Clean: ${clean}`);
|
|
95
98
|
console.log(` Triage.md orders tier-first (hard > soft > clean), then by the CLI`);
|
|
96
|
-
console.log(
|
|
99
|
+
console.log(scoreWeights.isDefault
|
|
100
|
+
? ` triage score (5xpaste + 5xcopy + 3xsidebar + 1xtab-away) within a tier.`
|
|
101
|
+
: ` triage score (${formulaText(scoreWeights.weights, 'x')}, from scoreWeights) within a tier.`);
|
|
97
102
|
|
|
98
103
|
// 4. Render outputs
|
|
99
104
|
console.log(`\nRendering...`);
|
|
@@ -107,6 +112,8 @@ export async function run(args) {
|
|
|
107
112
|
const { renderExtensions } = await import('./renderers/extensions.js');
|
|
108
113
|
|
|
109
114
|
await renderSummaryCSV(summaries, triage, config);
|
|
115
|
+
const { renderScoreWeights } = await import('./renderers/score-weights.js');
|
|
116
|
+
renderScoreWeights(config);
|
|
110
117
|
await renderTriage(triage, config);
|
|
111
118
|
await renderEventLog(participants, config);
|
|
112
119
|
await renderExtensions(participants, config);
|
|
@@ -35,7 +35,7 @@ import { buildReplayMeta } from '../replay/persistence.js';
|
|
|
35
35
|
class CyborgHunterReplayExtension {
|
|
36
36
|
static info = {
|
|
37
37
|
name: 'cyborg-hunter-replay',
|
|
38
|
-
version: '0.
|
|
38
|
+
version: '0.9.0',
|
|
39
39
|
data: {} // per-trial return is {}; session meta goes via addProperties
|
|
40
40
|
};
|
|
41
41
|
|
|
@@ -10,7 +10,7 @@ class CyborgHunterExtension {
|
|
|
10
10
|
// and package.json). Hand-bumped on each release; if you forget, the
|
|
11
11
|
// jsPsych developer console shows a stale number — the actual version
|
|
12
12
|
// attached to data is read from window.CyborgHunter.VERSION at runtime.
|
|
13
|
-
version: '0.
|
|
13
|
+
version: '0.9.0',
|
|
14
14
|
data: { integrity: { type: 'object' } }
|
|
15
15
|
};
|
|
16
16
|
|
package/src/shared/constants.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Default thresholds and preset configurations for Cyborg Hunter.
|
|
3
3
|
// Single source of truth — used by both the browser library and CLI report tool.
|
|
4
4
|
|
|
5
|
-
export const VERSION = "0.
|
|
5
|
+
export const VERSION = "0.9.0";
|
|
6
6
|
|
|
7
7
|
// Default detection thresholds shared across presets.
|
|
8
8
|
// Researchers can override any value at init() time.
|
package/src/shared/schema.js
CHANGED
|
@@ -34,6 +34,7 @@ export const TRIAL_REPORT_FIELDS = {
|
|
|
34
34
|
export const DEFAULT_CLI_CONFIG = {
|
|
35
35
|
dataDir: "./data",
|
|
36
36
|
replayDir: null, // replay artifacts dir; defaults to dataDir
|
|
37
|
+
scoreWeights: null, // report-score weights, merged per key onto the defaults (cli/analyzers/score-weights.js)
|
|
37
38
|
filePattern: "*.json",
|
|
38
39
|
participantIdField: "participantId",
|
|
39
40
|
trialIdField: "trialId",
|
package/src/shared/validation.js
CHANGED
|
@@ -95,7 +95,7 @@ function safeStr(v) {
|
|
|
95
95
|
* Finds the closest matching key using Levenshtein distance.
|
|
96
96
|
* Returns null if no key is within edit distance 3 (too different to suggest).
|
|
97
97
|
*/
|
|
98
|
-
function findClosestKey(input, keys) {
|
|
98
|
+
export function findClosestKey(input, keys) {
|
|
99
99
|
let best = null;
|
|
100
100
|
let bestDist = Infinity;
|
|
101
101
|
for (const key of keys) {
|