harnessgap 0.1.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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -0
  3. package/dist/adapter/correction.d.ts +14 -0
  4. package/dist/adapter/correction.js +68 -0
  5. package/dist/adapter/correction.js.map +1 -0
  6. package/dist/adapter/parse.d.ts +21 -0
  7. package/dist/adapter/parse.js +261 -0
  8. package/dist/adapter/parse.js.map +1 -0
  9. package/dist/adapter/scrub.d.ts +15 -0
  10. package/dist/adapter/scrub.js +97 -0
  11. package/dist/adapter/scrub.js.map +1 -0
  12. package/dist/adapter/stream.d.ts +14 -0
  13. package/dist/adapter/stream.js +207 -0
  14. package/dist/adapter/stream.js.map +1 -0
  15. package/dist/adapter/taxonomy.d.ts +7 -0
  16. package/dist/adapter/taxonomy.js +21 -0
  17. package/dist/adapter/taxonomy.js.map +1 -0
  18. package/dist/aggregate/leaderboard.d.ts +26 -0
  19. package/dist/aggregate/leaderboard.js +168 -0
  20. package/dist/aggregate/leaderboard.js.map +1 -0
  21. package/dist/cli.d.ts +2 -0
  22. package/dist/cli.js +67 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/config.d.ts +25 -0
  25. package/dist/config.js +191 -0
  26. package/dist/config.js.map +1 -0
  27. package/dist/detector/areas.d.ts +13 -0
  28. package/dist/detector/areas.js +95 -0
  29. package/dist/detector/areas.js.map +1 -0
  30. package/dist/detector/index.d.ts +14 -0
  31. package/dist/detector/index.js +31 -0
  32. package/dist/detector/index.js.map +1 -0
  33. package/dist/detector/record.d.ts +24 -0
  34. package/dist/detector/record.js +29 -0
  35. package/dist/detector/record.js.map +1 -0
  36. package/dist/detector/scoring.d.ts +21 -0
  37. package/dist/detector/scoring.js +140 -0
  38. package/dist/detector/scoring.js.map +1 -0
  39. package/dist/detector/signals.d.ts +6 -0
  40. package/dist/detector/signals.js +221 -0
  41. package/dist/detector/signals.js.map +1 -0
  42. package/dist/egress.d.ts +8 -0
  43. package/dist/egress.js +37 -0
  44. package/dist/egress.js.map +1 -0
  45. package/dist/git.d.ts +16 -0
  46. package/dist/git.js +59 -0
  47. package/dist/git.js.map +1 -0
  48. package/dist/index.d.ts +1 -0
  49. package/dist/index.js +4 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/output/calibrate.d.ts +35 -0
  52. package/dist/output/calibrate.js +149 -0
  53. package/dist/output/calibrate.js.map +1 -0
  54. package/dist/output/human.d.ts +24 -0
  55. package/dist/output/human.js +83 -0
  56. package/dist/output/human.js.map +1 -0
  57. package/dist/output/json.d.ts +17 -0
  58. package/dist/output/json.js +22 -0
  59. package/dist/output/json.js.map +1 -0
  60. package/dist/pipeline.d.ts +24 -0
  61. package/dist/pipeline.js +164 -0
  62. package/dist/pipeline.js.map +1 -0
  63. package/dist/types.d.ts +122 -0
  64. package/dist/types.js +4 -0
  65. package/dist/types.js.map +1 -0
  66. package/dist/walk.d.ts +12 -0
  67. package/dist/walk.js +92 -0
  68. package/dist/walk.js.map +1 -0
  69. package/package.json +49 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,qDAAqD;AACrD,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,0EAA0E;AAC1E,+DAA+D;AAE/D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,EAAE,OAAO,EAAoB,MAAM,eAAe,CAAC;AAG1D,yEAAyE;AACzE,2EAA2E;AAC3E,8CAA8C;AAC9C,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,iBAAiB,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAwB,CAAC;AAc5E,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,YAAY,CAAC;KAClB,WAAW,CAAC,+CAA+C,CAAC;KAC5D,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;AAExB,OAAO;KACJ,OAAO,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;KACpC,WAAW,CAAC,+CAA+C,CAAC;KAC5D,MAAM,CAAC,eAAe,EAAE,oCAAoC,CAAC;KAC7D,MAAM,CAAC,eAAe,EAAE,oDAAoD,CAAC;KAC7E,MAAM,CACL,aAAa,EACb,4BAA4B,EAC5B,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CACtC;KACA,MAAM,CAAC,QAAQ,EAAE,iDAAiD,CAAC;KACnE,MAAM,CAAC,aAAa,EAAE,gCAAgC,CAAC;KACvD,MAAM,CAAC,aAAa,EAAE,8BAA8B,CAAC;KACrD,MAAM,CAAC,iBAAiB,EAAE,uCAAuC,CAAC;KAClE,MAAM,CAAC,qBAAqB,EAAE,mDAAmD,CAAC;KAClF,MAAM,CAAC,KAAK,EAAE,IAAc,EAAE,EAAE;IAC/B,MAAM,QAAQ,GAAgB;QAC5B,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,UAAU,EAAE,IAAI,CAAC,MAAM;QACvB,SAAS,EAAE,IAAI,CAAC,SAAS;KAC1B,CAAC;IACF,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAC;QACvC,iEAAiE;QACjE,6BAA6B;QAC7B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,EAAE,GAAG,EAAE,CAC9C,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAC9B,CAAC;IACJ,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,uEAAuE;QACvE,8CAA8C;QAC9C,MAAM,GAAG,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACvD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,GAAG,IAAI,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACjE,CAAC;AACH,CAAC,CAAC,CAAC;AAEL,6EAA6E;AAC7E,6EAA6E;AAC7E,gFAAgF;AAChF,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE;IAC3C,MAAM,GAAG,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACvD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,GAAG,IAAI,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AACjE,CAAC,CAAC,CAAC"}
@@ -0,0 +1,25 @@
1
+ import type { Config } from './types.js';
2
+ /**
3
+ * Typed error for config problems. Carries a clean human-readable message only;
4
+ * the CLI is responsible for printing the message without leaking the stack.
5
+ */
6
+ export declare class ConfigError extends Error {
7
+ constructor(message: string);
8
+ }
9
+ /**
10
+ * The exact §7 defaults. Later tasks depend on these values verbatim.
11
+ */
12
+ export declare const DEFAULT_CONFIG: Config;
13
+ /**
14
+ * Parse a duration string like "30d" / "12h" / "5m" / "10s" into milliseconds.
15
+ * `undefined` and empty string mean "no window" and return `Infinity`.
16
+ * Throws `ConfigError` on unparseable input.
17
+ */
18
+ export declare function parseDuration(s: string | undefined): number;
19
+ /**
20
+ * Load and validate a config. With no `path`, looks for `.harnessgap.yml` in
21
+ * the cwd; if absent, returns `DEFAULT_CONFIG`. Deep-merges the file over the
22
+ * defaults (arrays replace). Throws `ConfigError` on: unreadable file, YAML
23
+ * parse error, unknown top-level keys, or out-of-range numeric fields.
24
+ */
25
+ export declare function loadConfig(path?: string): Config;
package/dist/config.js ADDED
@@ -0,0 +1,191 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { parse } from 'yaml';
4
+ /**
5
+ * Typed error for config problems. Carries a clean human-readable message only;
6
+ * the CLI is responsible for printing the message without leaking the stack.
7
+ */
8
+ export class ConfigError extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = 'ConfigError';
12
+ }
13
+ }
14
+ /**
15
+ * The exact §7 defaults. Later tasks depend on these values verbatim.
16
+ */
17
+ export const DEFAULT_CONFIG = {
18
+ detector: {
19
+ thresholds_as: 'percentile',
20
+ flag_pct: 90,
21
+ bootstrap_session_floor: 30,
22
+ bootstrap_flag_pct: 70,
23
+ reread_threshold: 5,
24
+ correction_window_ms: 120000,
25
+ signal_weights: {
26
+ explore_ratio: 1,
27
+ reread: 1,
28
+ failure_streak: 1,
29
+ corrections: 1,
30
+ abandonment: 0.5,
31
+ oscillation: 1.2,
32
+ wall_clock_per_line: 1,
33
+ },
34
+ bootstrap_thresholds: {
35
+ explore_ratio: 10,
36
+ reread: 5,
37
+ failure_streak: 3,
38
+ corrections: 2,
39
+ abandonment: true,
40
+ oscillation: 2,
41
+ wall_clock_per_line_ms: 300000,
42
+ },
43
+ },
44
+ areas: {
45
+ ignore: ['node_modules', 'build', 'target', 'dist', '.git', '.next', 'vendor'],
46
+ min_weight: 0.4,
47
+ min_depth: 2,
48
+ touch_weights: { edit: 3, read: 2, exec: 1 },
49
+ tail_fraction: 0.25,
50
+ explore_ratio_min: 0.8,
51
+ suppress_abandonment_when_no_exec: true,
52
+ test_cmd_patterns: [
53
+ 'test',
54
+ 'spec',
55
+ 'pytest',
56
+ 'npm test',
57
+ 'npm run test',
58
+ 'make',
59
+ 'cargo test',
60
+ 'go test',
61
+ 'jest',
62
+ 'vitest',
63
+ ],
64
+ },
65
+ };
66
+ const DURATION_RE = /^(\d+)([dhms])$/;
67
+ /**
68
+ * Parse a duration string like "30d" / "12h" / "5m" / "10s" into milliseconds.
69
+ * `undefined` and empty string mean "no window" and return `Infinity`.
70
+ * Throws `ConfigError` on unparseable input.
71
+ */
72
+ export function parseDuration(s) {
73
+ if (s === undefined || s === '')
74
+ return Infinity;
75
+ const m = DURATION_RE.exec(s);
76
+ if (!m)
77
+ throw new ConfigError(`Invalid duration: ${JSON.stringify(s)}`);
78
+ const n = Number(m[1]);
79
+ switch (m[2]) {
80
+ case 'd':
81
+ return n * 24 * 60 * 60 * 1000;
82
+ case 'h':
83
+ return n * 60 * 60 * 1000;
84
+ case 'm':
85
+ return n * 60 * 1000;
86
+ case 's':
87
+ return n * 1000;
88
+ default:
89
+ // Unreachable: regex guarantees one of d/h/m/s.
90
+ throw new ConfigError(`Invalid duration: ${JSON.stringify(s)}`);
91
+ }
92
+ }
93
+ const KNOWN_TOP_KEYS = new Set(['detector', 'areas']);
94
+ function isPlainObject(v) {
95
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
96
+ }
97
+ function isEnoent(e) {
98
+ return (typeof e === 'object' &&
99
+ e !== null &&
100
+ 'code' in e &&
101
+ e.code === 'ENOENT');
102
+ }
103
+ /**
104
+ * Deep-merge `override` over `base`. Objects merge recursively; arrays and
105
+ * primitives replace (arrays do NOT concatenate).
106
+ */
107
+ function deepMerge(base, override) {
108
+ if (!isPlainObject(override)) {
109
+ return override;
110
+ }
111
+ if (!isPlainObject(base)) {
112
+ return { ...override };
113
+ }
114
+ const result = { ...base };
115
+ for (const [k, v] of Object.entries(override)) {
116
+ if (k in base && isPlainObject(base[k]) && isPlainObject(v)) {
117
+ result[k] = deepMerge(base[k], v);
118
+ }
119
+ else {
120
+ result[k] = v;
121
+ }
122
+ }
123
+ return result;
124
+ }
125
+ function validateConfig(cfg) {
126
+ const d = cfg.detector;
127
+ if (d.flag_pct < 0 || d.flag_pct > 100) {
128
+ throw new ConfigError(`detector.flag_pct must be in [0,100], got ${d.flag_pct}`);
129
+ }
130
+ if (d.bootstrap_session_floor < 1) {
131
+ throw new ConfigError(`detector.bootstrap_session_floor must be >= 1, got ${d.bootstrap_session_floor}`);
132
+ }
133
+ for (const [k, v] of Object.entries(d.signal_weights)) {
134
+ if (v < 0) {
135
+ throw new ConfigError(`detector.signal_weights.${k} must be >= 0, got ${v}`);
136
+ }
137
+ }
138
+ const tw = cfg.areas.touch_weights;
139
+ for (const [k, v] of Object.entries(tw)) {
140
+ if (v < 0) {
141
+ throw new ConfigError(`areas.touch_weights.${k} must be >= 0, got ${v}`);
142
+ }
143
+ }
144
+ const mw = cfg.areas.min_weight;
145
+ if (mw < 0 || mw > 1) {
146
+ throw new ConfigError(`areas.min_weight must be in [0,1], got ${mw}`);
147
+ }
148
+ }
149
+ /**
150
+ * Load and validate a config. With no `path`, looks for `.harnessgap.yml` in
151
+ * the cwd; if absent, returns `DEFAULT_CONFIG`. Deep-merges the file over the
152
+ * defaults (arrays replace). Throws `ConfigError` on: unreadable file, YAML
153
+ * parse error, unknown top-level keys, or out-of-range numeric fields.
154
+ */
155
+ export function loadConfig(path) {
156
+ const target = path ?? resolve('.harnessgap.yml');
157
+ let raw;
158
+ try {
159
+ raw = readFileSync(target, 'utf8');
160
+ }
161
+ catch (e) {
162
+ // No path given and default file absent -> fail-open with defaults.
163
+ if (path === undefined && isEnoent(e)) {
164
+ return structuredClone(DEFAULT_CONFIG);
165
+ }
166
+ throw new ConfigError(`Cannot read config file: ${path ?? '.harnessgap.yml'}`);
167
+ }
168
+ let parsed;
169
+ try {
170
+ parsed = parse(raw);
171
+ }
172
+ catch (e) {
173
+ throw new ConfigError(`YAML parse error in ${path ?? '.harnessgap.yml'}: ${e.message}`);
174
+ }
175
+ // Empty file -> nothing to override -> defaults.
176
+ if (parsed === null || parsed === undefined) {
177
+ return structuredClone(DEFAULT_CONFIG);
178
+ }
179
+ if (!isPlainObject(parsed)) {
180
+ throw new ConfigError('Config must be a YAML object at the top level');
181
+ }
182
+ for (const k of Object.keys(parsed)) {
183
+ if (!KNOWN_TOP_KEYS.has(k)) {
184
+ throw new ConfigError(`Unknown config key: ${k}`);
185
+ }
186
+ }
187
+ const merged = deepMerge(DEFAULT_CONFIG, parsed);
188
+ validateConfig(merged);
189
+ return structuredClone(merged);
190
+ }
191
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { Config, NormalizedEvent } from '../types.js';
2
+ interface Area {
3
+ key: string;
4
+ weight: number;
5
+ }
6
+ /**
7
+ * Localize the areas touched by a session. See task-12 brief for the 5-step
8
+ * algorithm. Returns the deepest qualifying directories (repo-relative,
9
+ * forward-slash paths), each with `weight` = dir weight / total touch-weight in
10
+ * [0,1]. Returns `[]` when no directory qualifies (session is unlocalized).
11
+ */
12
+ export declare function localizeAreas(events: NormalizedEvent[], cfg: Config): Area[];
13
+ export {};
@@ -0,0 +1,95 @@
1
+ // Pure area localization: clusters a session's touched files into path-prefix
2
+ // areas using touch-weights, rolls up to ancestor directories, and returns the
3
+ // deepest qualifying directories (each capturing >= min_weight of the session's
4
+ // total touch-weight, at depth >= min_depth). No I/O, no mutation of inputs.
5
+ /**
6
+ * Localize the areas touched by a session. See task-12 brief for the 5-step
7
+ * algorithm. Returns the deepest qualifying directories (repo-relative,
8
+ * forward-slash paths), each with `weight` = dir weight / total touch-weight in
9
+ * [0,1]. Returns `[]` when no directory qualifies (session is unlocalized).
10
+ */
11
+ export function localizeAreas(events, cfg) {
12
+ const tw = cfg.areas.touch_weights;
13
+ const ignore = new Set(cfg.areas.ignore);
14
+ // Step 1: accumulate touch-weight per non-ignored file. Ignored paths
15
+ // (first segment in cfg.areas.ignore) contribute 0 weight — to files AND
16
+ // to the session total.
17
+ const fileWeights = new Map();
18
+ let total = 0;
19
+ for (const e of events) {
20
+ const w = touchWeight(e.tool, tw);
21
+ if (w === 0)
22
+ continue;
23
+ for (const f of e.input_digest.files) {
24
+ if (isIgnored(f, ignore))
25
+ continue;
26
+ fileWeights.set(f, (fileWeights.get(f) ?? 0) + w);
27
+ total += w;
28
+ }
29
+ }
30
+ if (total === 0)
31
+ return [];
32
+ // Step 2: roll up to ancestor directories. Each ancestor dir's weight = sum
33
+ // of weights of all files beneath it (transitively). For a file with segments
34
+ // [s0, s1, ..., sN], the ancestor dirs are s0, s0/s1, ..., s0/.../s(N-1).
35
+ const dirWeights = new Map();
36
+ for (const [file, w] of fileWeights) {
37
+ const segs = file.split('/');
38
+ for (let i = 1; i < segs.length; i++) {
39
+ const dir = segs.slice(0, i).join('/');
40
+ dirWeights.set(dir, (dirWeights.get(dir) ?? 0) + w);
41
+ }
42
+ }
43
+ // Step 3: candidates = dirs at depth >= min_depth capturing >= min_weight of
44
+ // total. Depth = number of `/`-separated segments from repo root.
45
+ const minWeight = cfg.areas.min_weight;
46
+ const minDepth = cfg.areas.min_depth;
47
+ const candidates = [];
48
+ for (const [dir, w] of dirWeights) {
49
+ if (dir.split('/').length < minDepth)
50
+ continue;
51
+ const fraction = w / total;
52
+ if (fraction < minWeight)
53
+ continue;
54
+ candidates.push({ key: dir, weight: fraction });
55
+ }
56
+ if (candidates.length === 0)
57
+ return [];
58
+ // Step 4: deepest pruning — drop any candidate that has a qualifying
59
+ // descendant. A descendant of D is any other candidate whose key starts with
60
+ // `D + '/'`. Siblings (no ancestor relationship) are both kept.
61
+ const result = [];
62
+ for (const c of candidates) {
63
+ const prefix = c.key + '/';
64
+ let hasDescendant = false;
65
+ for (const other of candidates) {
66
+ if (other.key !== c.key && other.key.startsWith(prefix)) {
67
+ hasDescendant = true;
68
+ break;
69
+ }
70
+ }
71
+ if (!hasDescendant)
72
+ result.push(c);
73
+ }
74
+ result.sort((a, b) => a.key.localeCompare(b.key));
75
+ return result;
76
+ }
77
+ /** Touch-weight for a tool kind: edit→3, read→2, exec→1; null/other→0. */
78
+ function touchWeight(tool, tw) {
79
+ switch (tool) {
80
+ case 'edit':
81
+ return tw.edit;
82
+ case 'read':
83
+ return tw.read;
84
+ case 'exec':
85
+ return tw.exec;
86
+ default:
87
+ return 0;
88
+ }
89
+ }
90
+ /** True when the path's first `/`-separated segment is in the ignore set. */
91
+ function isIgnored(path, ignore) {
92
+ const first = path.split('/', 1)[0] ?? '';
93
+ return ignore.has(first);
94
+ }
95
+ //# sourceMappingURL=areas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"areas.js","sourceRoot":"","sources":["../../src/detector/areas.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { Config, NormalizedEnvelope, StruggleRecord } from '../types.js';
2
+ /**
3
+ * Run the full detector pipeline over a batch of envelopes.
4
+ *
5
+ * 1. Compute signals per envelope (collect the full `SignalValues[]`).
6
+ * 2. Call `scoreSessions` ONCE with the full array (percentile needs the whole
7
+ * set to rank sessions against each other).
8
+ * 3. Per envelope: compute areas and assemble the `StruggleRecord`.
9
+ *
10
+ * Returns records in envelope order. The chosen `mode` is consistent across all
11
+ * records (`scoreSessions` picks one mode for the whole set); read it from any
12
+ * record's `mode` field. Empty envelopes → `[]` (no throw).
13
+ */
14
+ export declare function runDetector(envelopes: NormalizedEnvelope[], cfg: Config, forceBootstrap: boolean): StruggleRecord[];
@@ -0,0 +1,31 @@
1
+ // Detector orchestration: ties signals + scoring + areas together into one
2
+ // StruggleRecord per envelope. Pure: no I/O, no mutation of inputs.
3
+ //
4
+ // Orchestration order matters: percentile scoring needs the FULL signals set,
5
+ // so signals are computed for every envelope first, then `scoreSessions` is
6
+ // called ONCE with the whole array, then per-envelope areas + assembly run.
7
+ import { computeSignals } from './signals.js';
8
+ import { scoreSessions } from './scoring.js';
9
+ import { localizeAreas } from './areas.js';
10
+ import { assembleStruggleRecord } from './record.js';
11
+ /**
12
+ * Run the full detector pipeline over a batch of envelopes.
13
+ *
14
+ * 1. Compute signals per envelope (collect the full `SignalValues[]`).
15
+ * 2. Call `scoreSessions` ONCE with the full array (percentile needs the whole
16
+ * set to rank sessions against each other).
17
+ * 3. Per envelope: compute areas and assemble the `StruggleRecord`.
18
+ *
19
+ * Returns records in envelope order. The chosen `mode` is consistent across all
20
+ * records (`scoreSessions` picks one mode for the whole set); read it from any
21
+ * record's `mode` field. Empty envelopes → `[]` (no throw).
22
+ */
23
+ export function runDetector(envelopes, cfg, forceBootstrap) {
24
+ const signals = envelopes.map((e) => computeSignals(e.events, cfg));
25
+ const scores = scoreSessions({ signals, cfg, forceBootstrap });
26
+ return envelopes.map((env, i) => {
27
+ const areas = localizeAreas(env.events, cfg);
28
+ return assembleStruggleRecord(env, signals[i], scores[i], areas);
29
+ });
30
+ }
31
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/detector/index.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,oEAAoE;AACpE,EAAE;AACF,8EAA8E;AAC9E,4EAA4E;AAC5E,4EAA4E;AAG5E,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAErD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,WAAW,CACzB,SAA+B,EAC/B,GAAW,EACX,cAAuB;IAEvB,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IACpE,MAAM,MAAM,GAAG,aAAa,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,cAAc,EAAE,CAAC,CAAC;IAE/D,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE;QAC9B,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC7C,OAAO,sBAAsB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,24 @@
1
+ import type { NormalizedEnvelope, ScoringMode, SignalValues, StruggleRecord } from '../types.js';
2
+ /** Score shape produced by `scoreSessions` (composite accepted, not stored). */
3
+ interface SessionScore {
4
+ score_pct: number;
5
+ mode: ScoringMode;
6
+ flagged: boolean;
7
+ composite: number;
8
+ }
9
+ /** Area shape produced by `localizeAreas`. */
10
+ interface Area {
11
+ key: string;
12
+ weight: number;
13
+ }
14
+ /**
15
+ * Assemble a `StruggleRecord` by pure projection.
16
+ *
17
+ * - `signals`: raw `SignalValues` as-is (including nulls/booleans/raw counts).
18
+ * - `score_pct`/`mode`/`flagged`: from `score` (composite is NOT stored).
19
+ * - `truncated`/`event_count`/`started_at`/`duration_ms`/`session_id`/`repo`:
20
+ * from `envelope`.
21
+ * - `areas`: from the `areas` arg.
22
+ */
23
+ export declare function assembleStruggleRecord(envelope: NormalizedEnvelope, signals: SignalValues, score: SessionScore, areas: Area[]): StruggleRecord;
24
+ export {};
@@ -0,0 +1,29 @@
1
+ // Pure struggle-record projection. Assembles a StruggleRecord from its inputs
2
+ // without transformation: `signals` stored as-is (raw, including nulls/booleans);
3
+ // envelope fields copied verbatim; score fields projected (composite dropped —
4
+ // not on StruggleRecord). No I/O, no mutation of inputs.
5
+ /**
6
+ * Assemble a `StruggleRecord` by pure projection.
7
+ *
8
+ * - `signals`: raw `SignalValues` as-is (including nulls/booleans/raw counts).
9
+ * - `score_pct`/`mode`/`flagged`: from `score` (composite is NOT stored).
10
+ * - `truncated`/`event_count`/`started_at`/`duration_ms`/`session_id`/`repo`:
11
+ * from `envelope`.
12
+ * - `areas`: from the `areas` arg.
13
+ */
14
+ export function assembleStruggleRecord(envelope, signals, score, areas) {
15
+ return {
16
+ session_id: envelope.session_id,
17
+ repo: envelope.repo,
18
+ started_at: envelope.started_at,
19
+ duration_ms: envelope.duration_ms,
20
+ score_pct: score.score_pct,
21
+ mode: score.mode,
22
+ flagged: score.flagged,
23
+ truncated: envelope.truncated,
24
+ event_count: envelope.event_count,
25
+ areas,
26
+ signals,
27
+ };
28
+ }
29
+ //# sourceMappingURL=record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record.js","sourceRoot":"","sources":["../../src/detector/record.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,kFAAkF;AAClF,+EAA+E;AAC/E,yDAAyD;AAuBzD;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CACpC,QAA4B,EAC5B,OAAqB,EACrB,KAAmB,EACnB,KAAa;IAEb,OAAO;QACL,UAAU,EAAE,QAAQ,CAAC,UAAU;QAC/B,IAAI,EAAE,QAAQ,CAAC,IAAI;QACnB,UAAU,EAAE,QAAQ,CAAC,UAAU;QAC/B,WAAW,EAAE,QAAQ,CAAC,WAAW;QACjC,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,WAAW,EAAE,QAAQ,CAAC,WAAW;QACjC,KAAK;QACL,OAAO;KACR,CAAC;AACJ,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { Config, ScoringMode, SignalValues } from '../types.js';
2
+ interface SessionScore {
3
+ score_pct: number;
4
+ mode: ScoringMode;
5
+ flagged: boolean;
6
+ composite: number;
7
+ }
8
+ /**
9
+ * Score a batch of sessions. Pure: no I/O, no mutation of inputs. Returns one
10
+ * result per session, in input order.
11
+ *
12
+ * Mode precedence: `forceBootstrap` → bootstrap; else
13
+ * `thresholds_as === "absolute"` → bootstrap; else
14
+ * `signals.length < bootstrap_session_floor` → bootstrap; else percentile.
15
+ */
16
+ export declare function scoreSessions(input: {
17
+ signals: SignalValues[];
18
+ cfg: Config;
19
+ forceBootstrap: boolean;
20
+ }): SessionScore[];
21
+ export {};
@@ -0,0 +1,140 @@
1
+ // Pure scorer: percentile-of-composites and bootstrap modes. No I/O, no mutation.
2
+ // The seven signals, mapped from their `SignalValues` field to the (differently
3
+ // named) weight and threshold keys. Only `explore_ratio` and
4
+ // `wall_clock_per_line_ms` are nullable.
5
+ const SIGNAL_SPECS = [
6
+ { field: 'explore_ratio', weightKey: 'explore_ratio', thresholdKey: 'explore_ratio', boolean: false },
7
+ { field: 'reread', weightKey: 'reread', thresholdKey: 'reread', boolean: false },
8
+ { field: 'failure_streak', weightKey: 'failure_streak', thresholdKey: 'failure_streak', boolean: false },
9
+ { field: 'corrections', weightKey: 'corrections', thresholdKey: 'corrections', boolean: false },
10
+ { field: 'abandonment', weightKey: 'abandonment', thresholdKey: 'abandonment', boolean: true },
11
+ { field: 'oscillation', weightKey: 'oscillation', thresholdKey: 'oscillation', boolean: false },
12
+ { field: 'wall_clock_per_line_ms', weightKey: 'wall_clock_per_line', thresholdKey: 'wall_clock_per_line_ms', boolean: false },
13
+ ];
14
+ /**
15
+ * Score a batch of sessions. Pure: no I/O, no mutation of inputs. Returns one
16
+ * result per session, in input order.
17
+ *
18
+ * Mode precedence: `forceBootstrap` → bootstrap; else
19
+ * `thresholds_as === "absolute"` → bootstrap; else
20
+ * `signals.length < bootstrap_session_floor` → bootstrap; else percentile.
21
+ */
22
+ export function scoreSessions(input) {
23
+ const { signals, cfg, forceBootstrap } = input;
24
+ const mode = selectMode(signals.length, cfg, forceBootstrap);
25
+ if (mode === 'bootstrap') {
26
+ return signals.map((s) => bootstrapScore(s, cfg));
27
+ }
28
+ return percentileModeScore(signals, cfg);
29
+ }
30
+ function selectMode(n, cfg, forceBootstrap) {
31
+ if (forceBootstrap)
32
+ return 'bootstrap';
33
+ if (cfg.detector.thresholds_as === 'absolute')
34
+ return 'bootstrap';
35
+ if (n < cfg.detector.bootstrap_session_floor)
36
+ return 'bootstrap';
37
+ return 'percentile';
38
+ }
39
+ /**
40
+ * Percentile rank of the value at `index` within `values`:
41
+ * `(count strictly less than v) / (n−1) * 100` when n>1, else 0.
42
+ * Tied values each get `(count strictly less) / (n−1) * 100`.
43
+ */
44
+ function percentileRank(values, index) {
45
+ const n = values.length;
46
+ if (n <= 1)
47
+ return 0;
48
+ const v = values[index];
49
+ let less = 0;
50
+ for (let i = 0; i < n; i++) {
51
+ if (values[i] < v)
52
+ less += 1;
53
+ }
54
+ return (less / (n - 1)) * 100;
55
+ }
56
+ function percentileModeScore(signals, cfg) {
57
+ const n = signals.length;
58
+ const weights = cfg.detector.signal_weights;
59
+ // Per-session percentile rank for each numeric signal. Nullable signals are
60
+ // ranked only among sessions with non-null values; null sessions get no rank
61
+ // entry and are excluded from that signal's composite contribution.
62
+ const ranks = signals.map(() => ({}));
63
+ for (const spec of SIGNAL_SPECS) {
64
+ if (spec.boolean)
65
+ continue; // abandonment contributes 0/100 directly
66
+ const indices = [];
67
+ const values = [];
68
+ for (let i = 0; i < n; i++) {
69
+ const v = signals[i][spec.field];
70
+ if (v === null)
71
+ continue;
72
+ indices.push(i);
73
+ values.push(v);
74
+ }
75
+ const m = values.length;
76
+ if (m === 0)
77
+ continue; // all null — no contribution from this signal
78
+ for (let j = 0; j < m; j++) {
79
+ ranks[indices[j]][spec.weightKey] = percentileRank(values, j);
80
+ }
81
+ }
82
+ const composites = signals.map((s, i) => {
83
+ let weightSum = 0;
84
+ let weighted = 0;
85
+ for (const spec of SIGNAL_SPECS) {
86
+ const v = s[spec.field];
87
+ if (v === null)
88
+ continue;
89
+ const w = weights[spec.weightKey];
90
+ weightSum += w;
91
+ const contribution = spec.boolean ? (v ? 100 : 0) : (ranks[i][spec.weightKey] ?? 0);
92
+ weighted += contribution * w;
93
+ }
94
+ // Guard: if every weighted signal is null/absent, composite is 0.
95
+ if (weightSum === 0)
96
+ return 0;
97
+ return weighted / weightSum;
98
+ });
99
+ return composites.map((composite, i) => {
100
+ const scorePct = percentileRank(composites, i);
101
+ return {
102
+ score_pct: scorePct,
103
+ mode: 'percentile',
104
+ flagged: scorePct >= cfg.detector.flag_pct,
105
+ composite,
106
+ };
107
+ });
108
+ }
109
+ function bootstrapScore(s, cfg) {
110
+ const weights = cfg.detector.signal_weights;
111
+ const thresholds = cfg.detector.bootstrap_thresholds;
112
+ let weightSum = 0;
113
+ let weighted = 0;
114
+ let trippedCount = 0;
115
+ for (const spec of SIGNAL_SPECS) {
116
+ const v = s[spec.field];
117
+ if (v === null)
118
+ continue; // null signals neither trip nor count toward weights
119
+ const w = weights[spec.weightKey];
120
+ weightSum += w;
121
+ const threshold = thresholds[spec.thresholdKey];
122
+ const tripped = spec.boolean
123
+ ? v === true && threshold === true
124
+ : v >= threshold;
125
+ if (tripped) {
126
+ trippedCount += 1;
127
+ // composite is on a 0–100 scale: tripped contributes 100 × weight.
128
+ weighted += 100 * w;
129
+ }
130
+ }
131
+ const composite = weightSum === 0 ? 0 : weighted / weightSum;
132
+ const flagged = composite >= cfg.detector.bootstrap_flag_pct || trippedCount >= 2;
133
+ return {
134
+ score_pct: composite,
135
+ mode: 'bootstrap',
136
+ flagged,
137
+ composite,
138
+ };
139
+ }
140
+ //# sourceMappingURL=scoring.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scoring.js","sourceRoot":"","sources":["../../src/detector/scoring.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAsBlF,gFAAgF;AAChF,6DAA6D;AAC7D,yCAAyC;AACzC,MAAM,YAAY,GAA0B;IAC1C,EAAE,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,eAAe,EAAE,YAAY,EAAE,eAAe,EAAE,OAAO,EAAE,KAAK,EAAE;IACrG,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE;IAChF,EAAE,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,gBAAgB,EAAE,YAAY,EAAE,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE;IACxG,EAAE,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,OAAO,EAAE,KAAK,EAAE;IAC/F,EAAE,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE;IAC9F,EAAE,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,OAAO,EAAE,KAAK,EAAE;IAC/F,EAAE,KAAK,EAAE,wBAAwB,EAAE,SAAS,EAAE,qBAAqB,EAAE,YAAY,EAAE,wBAAwB,EAAE,OAAO,EAAE,KAAK,EAAE;CAC9H,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,KAI7B;IACC,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,cAAc,EAAE,GAAG,KAAK,CAAC;IAC/C,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,EAAE,cAAc,CAAC,CAAC;IAC7D,IAAI,IAAI,KAAK,WAAW,EAAE,CAAC;QACzB,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IACpD,CAAC;IACD,OAAO,mBAAmB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;AAC3C,CAAC;AAED,SAAS,UAAU,CAAC,CAAS,EAAE,GAAW,EAAE,cAAuB;IACjE,IAAI,cAAc;QAAE,OAAO,WAAW,CAAC;IACvC,IAAI,GAAG,CAAC,QAAQ,CAAC,aAAa,KAAK,UAAU;QAAE,OAAO,WAAW,CAAC;IAClE,IAAI,CAAC,GAAG,GAAG,CAAC,QAAQ,CAAC,uBAAuB;QAAE,OAAO,WAAW,CAAC;IACjE,OAAO,YAAY,CAAC;AACtB,CAAC;AAED;;;;GAIG;AACH,SAAS,cAAc,CAAC,MAAyB,EAAE,KAAa;IAC9D,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;IACxB,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IACrB,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IACxB,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC;YAAE,IAAI,IAAI,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC;AAChC,CAAC;AAED,SAAS,mBAAmB,CAAC,OAAuB,EAAE,GAAW;IAC/D,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IACzB,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,CAAC,cAAc,CAAC;IAE5C,4EAA4E;IAC5E,6EAA6E;IAC7E,oEAAoE;IACpE,MAAM,KAAK,GAA+C,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAElF,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;QAChC,IAAI,IAAI,CAAC,OAAO;YAAE,SAAS,CAAC,yCAAyC;QACrE,MAAM,OAAO,GAAa,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC3B,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACjC,IAAI,CAAC,KAAK,IAAI;gBAAE,SAAS;YACzB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAChB,MAAM,CAAC,IAAI,CAAC,CAAW,CAAC,CAAC;QAC3B,CAAC;QACD,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;QACxB,IAAI,CAAC,KAAK,CAAC;YAAE,SAAS,CAAC,8CAA8C;QACrE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC3B,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,cAAc,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAChE,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACtC,IAAI,SAAS,GAAG,CAAC,CAAC;QAClB,IAAI,QAAQ,GAAG,CAAC,CAAC;QACjB,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;YAChC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACxB,IAAI,CAAC,KAAK,IAAI;gBAAE,SAAS;YACzB,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAClC,SAAS,IAAI,CAAC,CAAC;YACf,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;YACpF,QAAQ,IAAI,YAAY,GAAG,CAAC,CAAC;QAC/B,CAAC;QACD,kEAAkE;QAClE,IAAI,SAAS,KAAK,CAAC;YAAE,OAAO,CAAC,CAAC;QAC9B,OAAO,QAAQ,GAAG,SAAS,CAAC;IAC9B,CAAC,CAAC,CAAC;IAEH,OAAO,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,CAAC,EAAE,EAAE;QACrC,MAAM,QAAQ,GAAG,cAAc,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;QAC/C,OAAO;YACL,SAAS,EAAE,QAAQ;YACnB,IAAI,EAAE,YAAY;YAClB,OAAO,EAAE,QAAQ,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ;YAC1C,SAAS;SACV,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,cAAc,CAAC,CAAe,EAAE,GAAW;IAClD,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,CAAC,cAAc,CAAC;IAC5C,MAAM,UAAU,GAAG,GAAG,CAAC,QAAQ,CAAC,oBAAoB,CAAC;IACrD,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,IAAI,YAAY,GAAG,CAAC,CAAC;IAErB,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;QAChC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxB,IAAI,CAAC,KAAK,IAAI;YAAE,SAAS,CAAC,qDAAqD;QAC/E,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAClC,SAAS,IAAI,CAAC,CAAC;QACf,MAAM,SAAS,GAAG,UAAU,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAChD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO;YAC1B,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,SAAS,KAAK,IAAI;YAClC,CAAC,CAAE,CAAY,IAAK,SAAoB,CAAC;QAC3C,IAAI,OAAO,EAAE,CAAC;YACZ,YAAY,IAAI,CAAC,CAAC;YAClB,mEAAmE;YACnE,QAAQ,IAAI,GAAG,GAAG,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IAED,MAAM,SAAS,GAAG,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,GAAG,SAAS,CAAC;IAC7D,MAAM,OAAO,GACX,SAAS,IAAI,GAAG,CAAC,QAAQ,CAAC,kBAAkB,IAAI,YAAY,IAAI,CAAC,CAAC;IAEpE,OAAO;QACL,SAAS,EAAE,SAAS;QACpB,IAAI,EAAE,WAAW;QACjB,OAAO;QACP,SAAS;KACV,CAAC;AACJ,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { Config, NormalizedEvent, SignalValues } from '../types.js';
2
+ /**
3
+ * Compute all detector signals for a normalized event stream. Pure: no I/O,
4
+ * no mutation of inputs. See field contracts in `types.ts` / the design spec.
5
+ */
6
+ export declare function computeSignals(events: NormalizedEvent[], cfg: Config): SignalValues;