cyborg-hunter 0.7.0 → 0.7.3

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.
@@ -0,0 +1,125 @@
1
+ // src/replay/viewer-model.js
2
+ // Wire SessionRecording → viewer model. Pure — no Node APIs — so a browser
3
+ // demo can bundle it directly (0.7.2 extraction from
4
+ // cli/renderers/replay-assets.js, which re-exports this for existing
5
+ // callers).
6
+ //
7
+ // This is one of exactly two allowed wire→viewer time-conversion points (the
8
+ // other lives in the CLI ingest path): SessionRecording carries ms since
9
+ // session start; the viewer speaks trial-relative ms.
10
+
11
+ /**
12
+ * Wire SessionRecording → viewer model. Times become trial-relative
13
+ * (t − t_load); null anchors (standalone implicit trials) degrade to the
14
+ * first event's time so durations are always finite.
15
+ */
16
+ export function buildViewerModel(recording) {
17
+ const md = recording.metadata || {};
18
+ const ext = recording.ch_extensions || {};
19
+
20
+ // ── Camera seeding (central, per Sol round-1 finding 8) ──
21
+ // New recordings carry a per-trial view_state seed. Legacy recordings
22
+ // don't — but the full event stream is present, so each trial's starting
23
+ // camera is reconstructed by folding all PRIOR trials' window-scroll and
24
+ // resize events over the session-start viewport. Initial scroll is assumed
25
+ // 0 (a recording that starts pre-scrolled with no scroll events is
26
+ // unrecoverable — that's what the viewer's legacy banner covers).
27
+ const vp = recording.viewport || {};
28
+ const vv = vp.visual_viewport || {};
29
+ // Scrollbar delta: legacy resize events carry only innerWidth/Height; the
30
+ // layout (client) width is estimated as w minus the session-start delta
31
+ // between innerWidth and the layout width (visual_viewport.width).
32
+ const sbW = (vp.width && (vp.client_width || vv.width))
33
+ ? vp.width - (vp.client_width || vv.width) : 0;
34
+ const sbH = (vp.height && (vp.client_height || vv.height))
35
+ ? vp.height - (vp.client_height || vv.height) : 0;
36
+ let foldState = {
37
+ x: 0, y: 0,
38
+ w: vp.width || null, h: vp.height || null,
39
+ cw: vp.client_width || vv.width || vp.width || null,
40
+ ch: vp.client_height || vv.height || vp.height || null,
41
+ dpr: vp.dpr || 1
42
+ };
43
+ const foldEvent = (state, e) => {
44
+ if (e.kind === 'scroll' && e.el == null && e.redacted == null) {
45
+ state.x = Number(e.x) || 0;
46
+ state.y = Number(e.y) || 0;
47
+ } else if (e.kind === 'resize') {
48
+ state.w = e.w != null ? e.w : state.w;
49
+ state.h = e.h != null ? e.h : state.h;
50
+ state.cw = e.cw != null ? e.cw : (e.w != null ? e.w - sbW : state.cw);
51
+ state.ch = e.ch != null ? e.ch : (e.h != null ? e.h - sbH : state.ch);
52
+ if (e.dpr != null) state.dpr = e.dpr;
53
+ }
54
+ return state;
55
+ };
56
+
57
+ // Defensive against malformed/truncated artifacts (a hand-edited or
58
+ // partially-written recording): non-array trials/events and null entries must
59
+ // degrade to empty rather than throw and abort the whole cohort report.
60
+ const rawTrials = Array.isArray(recording.trials) ? recording.trials : [];
61
+ const trials = rawTrials.map((trial) => {
62
+ trial = trial || {};
63
+ // Sort by absolute time before anchoring. RAF-coalesced input events flush
64
+ // with an EARLIER timestamp than events pushed after they were enqueued, so
65
+ // the recorded array is not strictly time-ordered; the viewer scrubs by
66
+ // scanning until the first future event and would otherwise mis-apply an
67
+ // out-of-order event on a seek. Stable sort keeps equal-time order.
68
+ const events = (Array.isArray(trial.events) ? trial.events : [])
69
+ .filter((e) => e && typeof e === 'object')
70
+ .slice()
71
+ .sort((a, b) => (Number(a.t) || 0) - (Number(b.t) || 0));
72
+ const anchor = trial.t_load != null ? trial.t_load
73
+ : (events.length > 0 ? events[0].t : 0);
74
+ const lastT = events.length > 0 ? events[events.length - 1].t : anchor;
75
+ const end = trial.t_end != null ? trial.t_end : lastT;
76
+ // Camera seed for THIS trial: recorded view_state, else the folded state
77
+ // as of the end of the previous trial. A recorded view_state also
78
+ // RESYNCS the fold — in a mixed recording (some trials seeded, some
79
+ // not: truncation, version mixes) a later unseeded trial must inherit
80
+ // real observed state, not a fold that ignored every observation.
81
+ if (trial.view_state) foldState = Object.assign({}, foldState, trial.view_state);
82
+ const camera = trial.view_state
83
+ ? Object.assign({}, trial.view_state, { source: 'view_state' })
84
+ : Object.assign({}, foldState, { source: 'folded' });
85
+ // Advance the fold across this trial's events for the NEXT trial's seed.
86
+ events.forEach((e) => foldEvent(foldState, e));
87
+ return {
88
+ index: trial.trial_index,
89
+ id: trial.trial_id,
90
+ plugin: trial.plugin,
91
+ durMs: Math.max(0, Math.round((end - anchor) * 10) / 10),
92
+ initialDom: trial.initial_dom || '',
93
+ camera,
94
+ events: events.map((e) => {
95
+ const out = Object.assign({}, e);
96
+ out.t = Math.max(0, Math.round(((Number(e.t) || 0) - anchor) * 10) / 10);
97
+ return out;
98
+ })
99
+ };
100
+ });
101
+
102
+ return {
103
+ pid: md.participant_id != null ? String(md.participant_id) : 'unknown',
104
+ tier: md.tier || 'trace',
105
+ keys: md.keys || null,
106
+ startTime: md.start_time || null,
107
+ endReason: md.end_reason || null,
108
+ recorder: md.recorder || null,
109
+ viewport: recording.viewport || null,
110
+ // Legacy = ANY trial lacks a view_state seed (all-legacy recordings and
111
+ // mixed/truncated ones alike): those trials replay on folded camera
112
+ // state, so the reduced-guarantees banner must show.
113
+ legacy: !rawTrials.every((t) => t && t.view_state),
114
+ markerAttr: ext.marker_attr || null,
115
+ // Session scrollbar delta (innerWidth − layout width): the viewer uses
116
+ // the same fallback chain as the folding above for legacy resize events.
117
+ scrollbar: { w: sbW, h: sbH },
118
+ stylesheets: (recording.stylesheets && recording.stylesheets.initial) || [],
119
+ scoring: ext.scoring || null,
120
+ guardViolations: ext.guard_violations || [],
121
+ captureStopped: !!ext.capture_stopped,
122
+ captureFailures: (ext.capture_failures || []).map((f) => f.channel),
123
+ trials
124
+ };
125
+ }
@@ -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.7.0";
5
+ export const VERSION = "0.7.3";
6
6
 
7
7
  // Default detection thresholds shared across presets.
8
8
  // Researchers can override any value at init() time.
@@ -0,0 +1,20 @@
1
+ // src/shared/paths.js
2
+ // Pure path-walking helper. No Node APIs — shared by CLI and browser preview.
3
+ // Used by: CLI ingest/renderers and the browser preview bundle.
4
+
5
+ // Resolves a possibly-dotted field path against an object (0.6.1).
6
+ // A flat key wins over a dotted walk, so data that literally contains a
7
+ // "metadata.sessionId" column stays addressable; otherwise the path is
8
+ // walked one segment at a time. Returns undefined when any segment is
9
+ // missing or a non-object is hit mid-path.
10
+ export function getByPath(obj, path) {
11
+ if (obj == null || typeof path !== 'string' || path.length === 0) return undefined;
12
+ if (Object.prototype.hasOwnProperty.call(obj, path)) return obj[path];
13
+ if (!path.includes('.')) return undefined;
14
+ let cur = obj;
15
+ for (const seg of path.split('.')) {
16
+ if (cur == null || typeof cur !== 'object') return undefined;
17
+ cur = cur[seg];
18
+ }
19
+ return cur;
20
+ }