cyborg-hunter 0.8.0 → 0.9.1

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 (41) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/CITATION.cff +2 -2
  3. package/README.md +6 -4
  4. package/dist/cyborg-hunter-replay.js +2 -2
  5. package/dist/cyborg-hunter.esm.js +3 -1
  6. package/dist/cyborg-hunter.min.js +2 -2
  7. package/dist/extension-cyborg-hunter.js +1 -1
  8. package/package.json +1 -1
  9. package/src/cli/analyzers/score-weights.js +145 -0
  10. package/src/cli/analyzers/triage.js +55 -39
  11. package/src/cli/config.js +15 -0
  12. package/src/cli/preview-entry.js +6 -0
  13. package/src/cli/renderers/font-face-css.js +16 -0
  14. package/src/cli/renderers/fonts/FONTS_MANIFEST.json +69 -0
  15. package/src/cli/renderers/fonts/majormonodisplay/OFL.txt +93 -0
  16. package/src/cli/renderers/fonts/majormonodisplay/majormonodisplay-400.woff2 +0 -0
  17. package/src/cli/renderers/fonts/recursive/OFL.txt +93 -0
  18. package/src/cli/renderers/fonts/recursive/recursive-300-1000.woff2 +0 -0
  19. package/src/cli/renderers/fonts/sofiasans/OFL.txt +93 -0
  20. package/src/cli/renderers/fonts/sofiasans/sofiasans-1-1000.woff2 +0 -0
  21. package/src/cli/renderers/fonts/sora/OFL.txt +93 -0
  22. package/src/cli/renderers/fonts/sora/sora-100-800.woff2 +0 -0
  23. package/src/cli/renderers/fonts/spacegrotesk/OFL.txt +93 -0
  24. package/src/cli/renderers/fonts/spacegrotesk/spacegrotesk-300-700.woff2 +0 -0
  25. package/src/cli/renderers/fonts/tomorrow/OFL.txt +93 -0
  26. package/src/cli/renderers/fonts/tomorrow/tomorrow-400.woff2 +0 -0
  27. package/src/cli/renderers/html-index-core.js +120 -106
  28. package/src/cli/renderers/html-index.js +7 -2
  29. package/src/cli/renderers/replay-styles.js +61 -0
  30. package/src/cli/renderers/replay-viewer.client.js +8 -6
  31. package/src/cli/renderers/report-fonts.js +24 -0
  32. package/src/cli/renderers/score-weights.js +16 -0
  33. package/src/cli/renderers/summary-csv.js +2 -0
  34. package/src/cli/renderers/trajectories-core.js +2 -1
  35. package/src/cli/renderers/triage-md.js +9 -2
  36. package/src/cli/report.js +10 -3
  37. package/src/jspsych/extension-cyborg-hunter-replay.js +1 -1
  38. package/src/jspsych/extension-cyborg-hunter.js +1 -1
  39. package/src/shared/constants.js +1 -1
  40. package/src/shared/schema.js +1 -0
  41. package/src/shared/validation.js +1 -1
@@ -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
- 'heuristic (5×paste + 5×copy + 3×sidebar + 1×tab-away) — it orders rows',
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) within each tier. Label both
90
- // explicitly so the console summary can't be read as "top N of triage.md".
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(` triage score (5xpaste + 5xcopy + 3xsidebar + 1xtab-away) within a tier.`);
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.8.0',
38
+ version: '0.9.1',
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.8.0',
13
+ version: '0.9.1',
14
14
  data: { integrity: { type: 'object' } }
15
15
  };
16
16
 
@@ -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.8.0";
5
+ export const VERSION = "0.9.1";
6
6
 
7
7
  // Default detection thresholds shared across presets.
8
8
  // Researchers can override any value at init() time.
@@ -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",
@@ -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) {