cyborg-hunter 0.7.4 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/CITATION.cff +2 -2
  3. package/README.md +21 -6
  4. package/dist/cyborg-hunter-replay.js +3 -3
  5. package/dist/cyborg-hunter.esm.js +3 -2
  6. package/dist/cyborg-hunter.min.js +3 -3
  7. package/dist/extension-cyborg-hunter.js +1 -1
  8. package/dist/extension-guard-friction.js +5 -5
  9. package/package.json +10 -2
  10. package/src/cli/ingest.js +311 -57
  11. package/src/cli/renderers/html-index-core.js +66 -12
  12. package/src/cli/renderers/html-index.js +7 -5
  13. package/src/cli/renderers/replay-assets.js +61 -7
  14. package/src/cli/renderers/replay-client-source.js +102 -0
  15. package/src/cli/renderers/replay-viewer.client.js +1750 -595
  16. package/src/cli/report.js +6 -0
  17. package/src/core/monitor.js +12 -13
  18. package/src/jspsych/extension-cyborg-hunter-replay.js +64 -3
  19. package/src/jspsych/extension-cyborg-hunter.js +1 -1
  20. package/src/jspsych/extension-guard-friction.js +59 -1
  21. package/src/replay/capture-dom.js +470 -458
  22. package/src/replay/capture-trace.js +688 -271
  23. package/src/replay/delivery.js +82 -0
  24. package/src/replay/dom-instantiate.js +779 -0
  25. package/src/replay/index.js +88 -5
  26. package/src/replay/initial-state.js +295 -0
  27. package/src/replay/mutations.js +668 -0
  28. package/src/replay/node-registry.js +116 -0
  29. package/src/replay/persistence.js +19 -6
  30. package/src/replay/recorder.js +342 -73
  31. package/src/replay/redaction.js +165 -0
  32. package/src/replay/serializer.js +148 -43
  33. package/src/replay/snapshot.js +409 -0
  34. package/src/replay/span.js +55 -0
  35. package/src/replay/viewer-model.js +293 -102
  36. package/src/shared/constants.js +1 -1
  37. package/src/shared/inline-safe.js +80 -0
  38. package/src/shared/schema-v2-validator.js +595 -0
  39. package/tools/convert/jspsych-v1-to-v2.mjs +432 -0
@@ -0,0 +1,116 @@
1
+ // src/replay/node-registry.js
2
+ // Node-ID registry for the v2 recording format. Pure — no imports, no DOM
3
+ // APIs — so it runs identically in the browser, in node tests against
4
+ // fake-DOM fixtures, and (later) inside the shared engine.
5
+ //
6
+ // Spec §4: every node in a recording carries an integer id; ids are scoped to
7
+ // a KEYFRAME SPAN (a keyframe segment plus its continuation segments) and are
8
+ // assigned in first-seen pre-order, restarting at 1 at each keyframe. The
9
+ // snapshot walk numbers the tree; mid-span `dom.add` subtrees continue the
10
+ // same counter (canonical-core pins ids 6,7 after a 5-node keyframe).
11
+ //
12
+ // Identity, not content: a WeakMap keyed by the LIVE node object. Nothing is
13
+ // stamped into the DOM (the v1 nonce-marker attribute mechanism, which had to
14
+ // dodge its own MutationObserver echo, is retired by this module), and a
15
+ // dropped span's entries become garbage-collectable the moment resetSpan()
16
+ // releases the map.
17
+
18
+ /**
19
+ * Create a node-ID registry for one keyframe span.
20
+ *
21
+ * @returns {{
22
+ * idFor: (node: object) => number,
23
+ * assignTree: (root: object) => number,
24
+ * peekId: (node: object) => number | null,
25
+ * resetSpan: () => void,
26
+ * count: number
27
+ * }}
28
+ */
29
+ export function createRegistry() {
30
+ let ids = new WeakMap();
31
+ let nextId = 1;
32
+
33
+ // Assign-if-absent for a SINGLE node (no descendant walk). In normal
34
+ // operation every node of the observed tree is numbered by assignTree
35
+ // first — snapshot walk at the keyframe, dom.add walks mid-span — so this
36
+ // is the fallback path for a node that WILL be serialized but has not been
37
+ // walked yet.
38
+ //
39
+ // NOT the way to resolve an event target or anchor: use peekId there. A
40
+ // target outside the observed root must resolve to null (design §2), and
41
+ // minting an id for it would put a number in the file that names nothing in
42
+ // the player's tree. dom.remove (Task 3) and anchors (Task 5) are peekId
43
+ // callers for the same reason.
44
+ //
45
+ // ids.set BEFORE the counter advances, so a call that throws (a non-object
46
+ // node) burns no id and leaves the span's numbering intact.
47
+ function idFor(node) {
48
+ const existing = ids.get(node);
49
+ if (existing !== undefined) return existing;
50
+ ids.set(node, nextId);
51
+ return nextId++;
52
+ }
53
+
54
+ // Pre-order walk: root, then each child's whole subtree, left to right.
55
+ // Numbers EVERY node the DomNode serialization can emit — elements, text,
56
+ // comments — because spec §4 gives all three an id.
57
+ //
58
+ // Boundary: the registry numbers whatever tree it is handed. Exclusion
59
+ // (data-record-exclude placeholders) and redaction filtering are the
60
+ // SERIALIZER's concern (Task 2); an excluded element still occupies its
61
+ // position and still needs an id, and the serializer decides what of the
62
+ // subtree reaches the file. Keeping the walk unconditional means ids stay
63
+ // stable regardless of what the serializer later chooses to emit.
64
+ //
65
+ // Precondition for the fixture's tidy 1..N numbering: the keyframe snapshot
66
+ // walk must be the FIRST allocation after each resetSpan(). Spec §4 only
67
+ // demands unique first-seen ids, and this registry stays correct either way
68
+ // (an idFor between reset and snapshot just shifts the tree to 2..N+1), but
69
+ // the canonical-core keyframe reads 1..5 only because nothing allocates
70
+ // ahead of its walk. Callers: reset, then snapshot, then everything else.
71
+ function assignTree(root) {
72
+ const rootId = idFor(root);
73
+ const children = root.childNodes;
74
+ if (children) {
75
+ for (let i = 0; i < children.length; i++) assignTree(children[i]);
76
+ }
77
+ return rootId;
78
+ }
79
+
80
+ // Read-only lookup: never allocates. Used wherever minting an id would be
81
+ // wrong rather than merely early — `dom.remove` targets, event anchors —
82
+ // because a fresh id there names a node the player was never sent.
83
+ //
84
+ // What it does NOT tell you: a non-null id does not mean the player has the
85
+ // node. assignTree numbers unconditionally, so descendants of a
86
+ // data-record-exclude placeholder, script/noscript elements the serializer
87
+ // skips, and anything numbered by an idFor fallback outside the observed
88
+ // root all peek non-null while appearing nowhere in the file. Strict
89
+ // validation only number-checks `node` fields, so a dangling reference
90
+ // survives validation and shows up as broken replay. Resolving a hit up to
91
+ // the nearest EMITTED ancestor (the placeholder, per spec §4) is the
92
+ // caller's job — this function answers "was it numbered", not "was it sent".
93
+ function peekId(node) {
94
+ const id = ids.get(node);
95
+ return id === undefined ? null : id;
96
+ }
97
+
98
+ // Keyframe boundary: drop the whole span's assignments and restart at 1.
99
+ // A fresh WeakMap rather than per-entry deletion — the old map is simply
100
+ // released along with the span it described.
101
+ function resetSpan() {
102
+ ids = new WeakMap();
103
+ nextId = 1;
104
+ }
105
+
106
+ return {
107
+ idFor,
108
+ assignTree,
109
+ peekId,
110
+ resetSpan,
111
+ // Ids are handed out only by the counter, one per node, so the number
112
+ // assigned in this span is always nextId − 1 — no second tally to keep in
113
+ // sync. A getter (not a snapshot property) so callers read it live.
114
+ get count() { return nextId - 1; },
115
+ };
116
+ }
@@ -10,14 +10,21 @@ var DATAPIPE_URL = 'https://pipe.jspsych.org/api/data/';
10
10
 
11
11
  import { sanitizeId } from '../shared/constants.js';
12
12
 
13
+ // The CH vendor namespace of a v2 recording (spec §9), where `metadata.tier`
14
+ // and the `ch_extensions` diagnostics moved.
15
+ function chExt(recording) {
16
+ var ext = recording && recording.extensions;
17
+ return (ext && ext['cyborg-hunter']) || {};
18
+ }
19
+
13
20
  /**
14
- * <pid>-replay-<sessionStartEpochMs>.json — the epoch suffix makes reloads
21
+ * <pid>-replay-<recordingStartEpochMs>.json — the epoch suffix makes reloads
15
22
  * produce distinct artifacts instead of overwriting (CLI picks the latest
16
23
  * and warns on multiples).
17
24
  */
18
25
  export function replayFilename(recording) {
19
- var pid = sanitizeId(recording.metadata.participant_id);
20
- var epoch = Date.parse(recording.metadata.start_time);
26
+ var pid = sanitizeId(recording.participant_id);
27
+ var epoch = Date.parse(recording.recording_started_at);
21
28
  return pid + '-replay-' + epoch + '.json';
22
29
  }
23
30
 
@@ -25,16 +32,22 @@ export function replayFilename(recording) {
25
32
  * The small pointer that rides along in the jsPsych data (one
26
33
  * addProperties call) so the analyst can tell from the CSV alone whether a
27
34
  * replay exists, how big it was, and where it went.
35
+ *
36
+ * The OUTPUT shape is a stable contract with the CLI ingest and the HTML index
37
+ * (they read these six keys); only where the values are read FROM moved to v2.
28
38
  */
29
39
  export function buildReplayMeta(recording, savedTo) {
40
+ var ext = chExt(recording);
30
41
  return {
31
42
  schema_version: recording.schema_version,
32
- tier: recording.metadata.tier,
43
+ tier: ext.tier != null ? ext.tier : null,
33
44
  saved_to: savedTo,
34
45
  bytes_uncompressed: JSON.stringify(recording).length,
35
- capture_failures: (recording.ch_extensions.capture_failures || [])
46
+ capture_failures: (ext.capture_failures || [])
36
47
  .map(function (f) { return f.channel; }),
37
- capture_stopped: !!recording.ch_extensions.capture_stopped
48
+ // Spec §5.7's standard flag, which mirrors the vendor one; reading the
49
+ // standard field keeps the pointer honest for any v2 producer.
50
+ capture_stopped: !!recording.truncated
38
51
  };
39
52
  }
40
53