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,97 @@
1
+ // Pattern-catalog secret scrubber — the privacy backbone for slice-2 persistence.
2
+ // Pure functions, no I/O, no entropy heuristic. Seven rules applied in a fixed
3
+ // order so earlier rules (heredoc blocks) are not partially mangled by later
4
+ // ones (env-var). The same sentinel literal is used everywhere.
5
+ const SENTINEL = '***REDACTED***';
6
+ const MAX_CMD_QUERY = 512;
7
+ const MAX_FILES = 50;
8
+ /**
9
+ * Apply the full 7-rule pattern catalog to a command/query string. Rules run in
10
+ * the documented order: heredoc keys → env-vars → auth headers → URL creds →
11
+ * flag secrets → credential-file paths → known-format tokens.
12
+ */
13
+ function scrubCatalog(s) {
14
+ // (1) Heredoc / inline private keys — replace the entire PEM block (BEGIN
15
+ // through END, inclusive) with the sentinel.
16
+ s = s.replace(/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*-----/g, SENTINEL);
17
+ // (2) Env-var assignments — keep KEY= when KEY is UPPERCASE_WITH_UNDERSCORES;
18
+ // redact the value. Optional export/set/env prefix is preserved.
19
+ s = s.replace(/(^|\s)((?:export|set|env)\s+)?([A-Z][A-Z0-9_]*)=(?:"[^"]*"|'[^']*'|\S+)/g, (_m, pre, prefix, key) => `${pre}${prefix ?? ''}${key}=${SENTINEL}`);
20
+ // (3) Authorization headers and bare Bearer tokens. Case-insensitive: HTTP
21
+ // headers are case-insensitive (RFC 7230), so `authorization: bearer`
22
+ // must scrub the same as the canonical form.
23
+ s = s.replace(/Authorization:\s*[^\n'"]*/gi, `Authorization: ${SENTINEL}`);
24
+ s = s.replace(/Bearer\s+[A-Za-z0-9._+/=-]+/gi, `Bearer ${SENTINEL}`);
25
+ // (4) URL-embedded credentials — ://user:pass@, ://token@, or ://:pass@
26
+ // (empty username, e.g. redis://:s3cr3t@host) → ://***REDACTED***@.
27
+ // Host/scheme kept; rejects a bare ://@ with no credential component.
28
+ s = s.replace(/(:\/\/)(?:[^\s/@:]+(?::[^\s/@]+)?|:[^\s/@]+)@/g, `$1${SENTINEL}@`);
29
+ // (5) Flag secrets — -p <val>, -u <user:pass>, --password/--secret/--token/
30
+ // --api-key/--access-key (case-insensitive). Value redacted, flag kept.
31
+ s = s.replace(/(^|\s)(--password|--secret|--token|--api-key|--access-key|-p|-u)\s+(\S+)/gi, (_m, pre, flag) => `${pre}${flag} ${SENTINEL}`);
32
+ s = s.replace(/(^|\s)(--password|--secret|--token|--api-key|--access-key)=("[^"]*"|'[^']*'|\S+)/gi, (_m, pre, flag) => `${pre}${flag}=${SENTINEL}`);
33
+ // (6) Credential-file path globs — check each whitespace/quote-delimited
34
+ // token; if it is a credential-file path, replace with sentinel.
35
+ s = s.replace(/[^\s'"]+/g, (token) => isCredentialFile(token) ? SENTINEL : token);
36
+ // (7) Known-format tokens — AWS access-key IDs, GitHub tokens, Slack tokens,
37
+ // JWTs.
38
+ s = s.replace(/AKIA[0-9A-Z]{16}/g, SENTINEL);
39
+ s = s.replace(/gh[oprs]_[A-Za-z0-9]{36}/g, SENTINEL);
40
+ s = s.replace(/xox[baprs]-[A-Za-z0-9-]+/g, SENTINEL);
41
+ s = s.replace(/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, SENTINEL);
42
+ return s;
43
+ }
44
+ /**
45
+ * Test whether a path matches a credential-file glob from the catalog.
46
+ * Globs (basename unless a directory suffix is noted): .env, *.pem, *.key,
47
+ * .aws/credentials (path suffix), .npmrc, id_rsa*, .pgpass, .htpasswd,
48
+ * service-account*.json, credentials.json.
49
+ */
50
+ function isCredentialFile(p) {
51
+ const base = p.split('/').pop() ?? p;
52
+ if (base === '.env')
53
+ return true;
54
+ if (base.endsWith('.pem'))
55
+ return true;
56
+ if (base.endsWith('.key'))
57
+ return true;
58
+ if (p.endsWith('.aws/credentials'))
59
+ return true;
60
+ if (base === '.npmrc')
61
+ return true;
62
+ if (base.startsWith('id_rsa'))
63
+ return true;
64
+ if (base === '.pgpass')
65
+ return true;
66
+ if (base === '.htpasswd')
67
+ return true;
68
+ if (/^service-account.*\.json$/.test(base))
69
+ return true;
70
+ if (base === 'credentials.json')
71
+ return true;
72
+ return false;
73
+ }
74
+ /**
75
+ * Scrub an exec command string: apply the full catalog, then truncate to 512
76
+ * chars (no marker). Replacements use the fixed sentinel `***REDACTED***`.
77
+ */
78
+ export function scrubCmd(cmd) {
79
+ return scrubCatalog(cmd).slice(0, MAX_CMD_QUERY);
80
+ }
81
+ /**
82
+ * Scrub a search/query string: same catalog and length cap as `scrubCmd`.
83
+ */
84
+ export function scrubQuery(q) {
85
+ return scrubCatalog(q).slice(0, MAX_CMD_QUERY);
86
+ }
87
+ /**
88
+ * Scrub a list of file paths: replace any path matching a credential-file glob
89
+ * with the sentinel, leave others unchanged, then drop entries beyond 50 (no
90
+ * marker). Only the credential-file rule applies here — not the full catalog.
91
+ */
92
+ export function scrubFiles(files) {
93
+ return files
94
+ .map((f) => (isCredentialFile(f) ? SENTINEL : f))
95
+ .slice(0, MAX_FILES);
96
+ }
97
+ //# sourceMappingURL=scrub.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrub.js","sourceRoot":"","sources":["../../src/adapter/scrub.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { NormalizedEnvelope, Warnings } from '../types.js';
2
+ type StreamWarnings = Pick<Warnings, 'malformed_lines' | 'oversized_lines' | 'truncated_sessions'>;
3
+ /**
4
+ * Stream one .jsonl transcript → { envelope, cwd, warnings }. Reads line-by-line
5
+ * via node:readline (never slurps). Enforces 1 MB line / 5000 event / 50 MB byte
6
+ * caps. Fail-open: malformed JSON and oversized lines are skipped + counted,
7
+ * never thrown. Deterministic given the file.
8
+ */
9
+ export declare function streamSession(filePath: string): Promise<{
10
+ envelope: NormalizedEnvelope;
11
+ cwd: string;
12
+ warnings: StreamWarnings;
13
+ }>;
14
+ export {};
@@ -0,0 +1,207 @@
1
+ // Streaming transcript reader — the only I/O in the adapter. Reads one .jsonl
2
+ // transcript file line-by-line (never slurps the whole file), enforces size
3
+ // caps, calls normalizeRecord per parsed line, threads ctx.prevToolCall through
4
+ // records in order, merges tool_use + tool_result pairs into single tool_call
5
+ // events (see mergeToolCalls), and returns a NormalizedEnvelope + the session's
6
+ // representative cwd + warning counts. Fail-open: never throws on bad input
7
+ // (malformed/oversized/truncated); skips and counts instead.
8
+ //
9
+ // Field paths: `cwd` is a top-level string on most user/assistant records
10
+ // (confirmed from real transcripts; documented in test/parse.test.ts). We read
11
+ // it from the raw parsed record BEFORE normalizeRecord (which does not emit it).
12
+ import { createReadStream } from 'node:fs';
13
+ import * as readline from 'node:readline';
14
+ import * as path from 'node:path';
15
+ import { normalizeRecord } from './parse.js';
16
+ // --- Caps (verbatim from the task brief) ---
17
+ const LINE_CAP = 1_048_576; // 1 MB: lines over this are skipped (oversized_lines++)
18
+ const EVENT_CAP = 5000; // once 5000 events collected, drop the rest (truncated)
19
+ const BYTE_CAP = 52_428_800; // 50 MB: stop reading once cumulative bytes ≥ this (truncated)
20
+ /**
21
+ * Stream one .jsonl transcript → { envelope, cwd, warnings }. Reads line-by-line
22
+ * via node:readline (never slurps). Enforces 1 MB line / 5000 event / 50 MB byte
23
+ * caps. Fail-open: malformed JSON and oversized lines are skipped + counted,
24
+ * never thrown. Deterministic given the file.
25
+ */
26
+ export async function streamSession(filePath) {
27
+ const events = [];
28
+ let malformed_lines = 0;
29
+ let oversized_lines = 0;
30
+ let truncated = false;
31
+ let cumulativeBytes = 0;
32
+ let cwd = '';
33
+ // Session span tracked across ALL parsed records (for envelope.duration_ms /
34
+ // started_at), regardless of whether they became kept events. Result records'
35
+ // timestamps are included so the span reflects the full session end-to-end.
36
+ let firstRecordT = null;
37
+ let firstRecordTs = null;
38
+ let lastRecordT = null;
39
+ const ctx = { prevToolCall: null };
40
+ // session_id = filename stem (basename without the last extension).
41
+ const session_id = path.basename(filePath).replace(/\.[^.]+$/, '');
42
+ try {
43
+ const rl = readline.createInterface({
44
+ input: createReadStream(filePath, { encoding: 'utf8' }),
45
+ crlfDelay: Infinity,
46
+ });
47
+ try {
48
+ for await (const line of rl) {
49
+ // Bytes read for this line (UTF-8) + 1 for the stripped newline.
50
+ const lineBytes = Buffer.byteLength(line, 'utf8') + 1;
51
+ cumulativeBytes += lineBytes;
52
+ if (lineBytes > LINE_CAP) {
53
+ oversized_lines++;
54
+ }
55
+ else {
56
+ let parsed;
57
+ let parseOk = true;
58
+ try {
59
+ parsed = JSON.parse(line);
60
+ }
61
+ catch {
62
+ parseOk = false;
63
+ }
64
+ if (parseOk) {
65
+ if (parsed !== null && typeof parsed === 'object') {
66
+ const rec = parsed;
67
+ // Extract cwd from the first record that carries a non-empty one.
68
+ if (cwd === '') {
69
+ const c = rec.cwd;
70
+ if (typeof c === 'string' && c.length > 0)
71
+ cwd = c;
72
+ }
73
+ // Track session span across ALL parsed records (system records,
74
+ // tool_use, tool_result — regardless of whether they became kept
75
+ // events), so duration_ms reflects the full session.
76
+ const ts = rec.timestamp;
77
+ if (typeof ts === 'string') {
78
+ const ms = Date.parse(ts);
79
+ if (!Number.isNaN(ms)) {
80
+ if (firstRecordT === null) {
81
+ firstRecordT = ms;
82
+ firstRecordTs = ts;
83
+ }
84
+ lastRecordT = ms;
85
+ }
86
+ }
87
+ }
88
+ const ev = normalizeRecord(parsed, ctx);
89
+ if (ev !== null) {
90
+ if (events.length < EVENT_CAP) {
91
+ events.push(ev);
92
+ // Thread prevToolCall: the most recent assistant tool_use (non-null
93
+ // tool) is what the next user_msg sees.
94
+ if (ev.kind === 'tool_call' && ev.tool !== null) {
95
+ ctx.prevToolCall = { tool: ev.tool };
96
+ }
97
+ }
98
+ else {
99
+ truncated = true;
100
+ }
101
+ }
102
+ }
103
+ else {
104
+ malformed_lines++;
105
+ }
106
+ }
107
+ // Stop conditions (checked after each line).
108
+ if (events.length >= EVENT_CAP) {
109
+ truncated = true;
110
+ break;
111
+ }
112
+ if (cumulativeBytes >= BYTE_CAP) {
113
+ truncated = true;
114
+ break;
115
+ }
116
+ }
117
+ }
118
+ finally {
119
+ rl.close();
120
+ }
121
+ }
122
+ catch {
123
+ // Stream/read error (missing/unreadable file, etc.) — fail open: return
124
+ // whatever was collected so far, no throw.
125
+ }
126
+ // Merge tool_use + tool_result pairs into single tool_call events so downstream
127
+ // signals can see tool + ok on the same event (see mergeToolCalls docs).
128
+ const merged = mergeToolCalls(events);
129
+ const started_at = firstRecordTs ?? (merged.length > 0 ? merged[0].t : '');
130
+ const envelope = {
131
+ schema_version: 1,
132
+ session_id,
133
+ agent: 'claude-code',
134
+ repo: '',
135
+ started_at,
136
+ duration_ms: firstRecordT !== null && lastRecordT !== null ? lastRecordT - firstRecordT : 0,
137
+ events: merged,
138
+ truncated,
139
+ event_count: merged.length,
140
+ };
141
+ return {
142
+ envelope,
143
+ cwd,
144
+ warnings: {
145
+ malformed_lines,
146
+ oversized_lines,
147
+ truncated_sessions: truncated ? 1 : 0,
148
+ },
149
+ };
150
+ }
151
+ /**
152
+ * Post-process: merge each tool_use + tool_result pair into ONE tool_call event.
153
+ *
154
+ * `normalizeRecord` (pure, single-record) emits two events per tool invocation:
155
+ * - tool_use → tool_call (tool !== null, ok=true placeholder, interrupted=false)
156
+ * - tool_result → tool_call (tool=null, ok=!is_error, interrupted=result)
157
+ * Downstream signals need tool + ok on the SAME event (e.g. failure_streak filters
158
+ * `tool==='exec' && ok===false`). This merge closes that gap.
159
+ *
160
+ * Pairing: each tool_result is paired with the most recent unresolved tool_use
161
+ * (stack — correct for Claude Code's sequential tool_use→result ordering, where
162
+ * each call is immediately followed by its own result). The result's
163
+ * ok/interrupted/duration_ms/t are merged onto the tool_use event; the result
164
+ * event is dropped from the list.
165
+ *
166
+ * Orphan results (no preceding unresolved tool_use) → dropped.
167
+ * Tool_uses with no result (session interrupted before result arrived) → kept
168
+ * with the placeholder ok=true, interrupted=false.
169
+ *
170
+ * Time handling: when paired, the merged event's `t` is set to the RESULT's `t`
171
+ * (not the tool_use's `t`). This keeps the session span correct for the
172
+ * wall_clock_per_line signal (which uses `events[last].t − events[0].t`): the
173
+ * last result's time is preserved on the merged event even though the result
174
+ * event itself is removed. Without this, dropping result events would shrink the
175
+ * computed span. When unpaired (no result), `t` stays the tool_use's time.
176
+ */
177
+ function mergeToolCalls(events) {
178
+ const result = [];
179
+ // Stack of indices into `result` pointing at unresolved tool_use events.
180
+ const unresolved = [];
181
+ for (const ev of events) {
182
+ if (ev.kind === 'tool_call' && ev.tool === null) {
183
+ // tool_result event: merge onto the most recent unresolved tool_use.
184
+ const idx = unresolved.pop();
185
+ if (idx === undefined)
186
+ continue; // orphan result → drop
187
+ const use = result[idx];
188
+ result[idx] = {
189
+ ...use,
190
+ ok: ev.ok,
191
+ interrupted: ev.interrupted,
192
+ duration_ms: ev.duration_ms,
193
+ t: ev.t, // result time preserves the session span
194
+ };
195
+ }
196
+ else {
197
+ result.push(ev);
198
+ if (ev.kind === 'tool_call' && ev.tool !== null) {
199
+ // tool_use event: track for later merging with its result.
200
+ unresolved.push(result.length - 1);
201
+ }
202
+ }
203
+ }
204
+ // Unresolved tool_uses remain with ok=true placeholder, interrupted=false.
205
+ return result;
206
+ }
207
+ //# sourceMappingURL=stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.js","sourceRoot":"","sources":["../../src/adapter/stream.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { ToolKind } from '../types.js';
2
+ /**
3
+ * Map a Claude Code tool name to a ToolKind.
4
+ * Unknown names (Task*, WebSearch, WebFetch, mcp__*, etc.) and empty strings
5
+ * map to 'other'. Case-sensitive: Claude Code tool names are PascalCase.
6
+ */
7
+ export declare function mapToolKind(ccToolName: string): ToolKind;
@@ -0,0 +1,21 @@
1
+ // Claude Code tool-name → ToolKind map. Pure lookup, no I/O.
2
+ // Pinned to the six ToolKind values in src/types.ts. Unknown / empty → 'other'.
3
+ const TOOL_KIND_MAP = {
4
+ Read: 'read',
5
+ Grep: 'search',
6
+ Glob: 'search',
7
+ LS: 'list',
8
+ Edit: 'edit',
9
+ Write: 'edit',
10
+ NotebookEdit: 'edit',
11
+ Bash: 'exec',
12
+ };
13
+ /**
14
+ * Map a Claude Code tool name to a ToolKind.
15
+ * Unknown names (Task*, WebSearch, WebFetch, mcp__*, etc.) and empty strings
16
+ * map to 'other'. Case-sensitive: Claude Code tool names are PascalCase.
17
+ */
18
+ export function mapToolKind(ccToolName) {
19
+ return TOOL_KIND_MAP[ccToolName] ?? 'other';
20
+ }
21
+ //# sourceMappingURL=taxonomy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"taxonomy.js","sourceRoot":"","sources":["../../src/adapter/taxonomy.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,gFAAgF;AAIhF,MAAM,aAAa,GAA6B;IAC9C,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,QAAQ;IACd,IAAI,EAAE,QAAQ;IACd,EAAE,EAAE,MAAM;IACV,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE,MAAM;IACb,YAAY,EAAE,MAAM;IACpB,IAAI,EAAE,MAAM;CACb,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,UAAkB;IAC5C,OAAO,aAAa,CAAC,UAAU,CAAC,IAAI,OAAO,CAAC;AAC9C,CAAC"}
@@ -0,0 +1,26 @@
1
+ import type { AreaRow, Config, StruggleRecord } from '../types.js';
2
+ /**
3
+ * Aggregate per-session `StruggleRecord`s into per-area `AreaRow`s plus a summary.
4
+ *
5
+ * 1. For each record, for each of its `areas`, add the record (weighted by
6
+ * `area.weight`) to that area's `sessions_total`; if `flagged`, add to
7
+ * `sessions_flagged` (weighted).
8
+ * 2. `mean_score` = average of `score_pct` over flagged records touching the
9
+ * area (0 if none flagged).
10
+ * 3. `top_signals` (per area, max 3): median raw signal value across the area's
11
+ * flagged sessions, top 3 by value desc, `display` formatted per kind.
12
+ * 4. Sort rows by `sessions_flagged` desc, then `mean_score` desc.
13
+ * 5. `summary.unlocalized` = count of records with empty `areas`;
14
+ * `flagged`/`unflagged` = counts of all records (unlocalized included).
15
+ *
16
+ * `cfg` is part of the signature contract but unused in v1 (raw medians; no
17
+ * weighting knobs read). Reserved for future ranking configuration.
18
+ */
19
+ export declare function aggregateAreas(records: StruggleRecord[], cfg: Config): {
20
+ rows: AreaRow[];
21
+ summary: {
22
+ flagged: number;
23
+ unflagged: number;
24
+ unlocalized: number;
25
+ };
26
+ };
@@ -0,0 +1,168 @@
1
+ // Pure area aggregation: rolls per-session StruggleRecords up into per-area
2
+ // AreaRows (weighted counting, mean_score, top_signals) plus a summary.
3
+ // No I/O, no mutation of inputs.
4
+ // The seven signals in a fixed canonical order. Ties in the top-signals ranking
5
+ // preserve this order (stable sort), so output is deterministic.
6
+ const SIGNAL_SPECS = [
7
+ { name: 'explore_ratio', field: 'explore_ratio', kind: 'ratio' },
8
+ { name: 'reread', field: 'reread', kind: 'count' },
9
+ { name: 'failure_streak', field: 'failure_streak', kind: 'count' },
10
+ { name: 'corrections', field: 'corrections', kind: 'count' },
11
+ { name: 'abandonment', field: 'abandonment', kind: 'boolean' },
12
+ { name: 'oscillation', field: 'oscillation', kind: 'count' },
13
+ { name: 'wall_clock_per_line', field: 'wall_clock_per_line_ms', kind: 'duration' },
14
+ ];
15
+ /**
16
+ * Aggregate per-session `StruggleRecord`s into per-area `AreaRow`s plus a summary.
17
+ *
18
+ * 1. For each record, for each of its `areas`, add the record (weighted by
19
+ * `area.weight`) to that area's `sessions_total`; if `flagged`, add to
20
+ * `sessions_flagged` (weighted).
21
+ * 2. `mean_score` = average of `score_pct` over flagged records touching the
22
+ * area (0 if none flagged).
23
+ * 3. `top_signals` (per area, max 3): median raw signal value across the area's
24
+ * flagged sessions, top 3 by value desc, `display` formatted per kind.
25
+ * 4. Sort rows by `sessions_flagged` desc, then `mean_score` desc.
26
+ * 5. `summary.unlocalized` = count of records with empty `areas`;
27
+ * `flagged`/`unflagged` = counts of all records (unlocalized included).
28
+ *
29
+ * `cfg` is part of the signature contract but unused in v1 (raw medians; no
30
+ * weighting knobs read). Reserved for future ranking configuration.
31
+ */
32
+ export function aggregateAreas(records, cfg) {
33
+ void cfg; // reserved; v1 does not read cfg
34
+ const byArea = new Map();
35
+ let flagged = 0;
36
+ let unflagged = 0;
37
+ let unlocalized = 0;
38
+ for (const rec of records) {
39
+ if (rec.flagged)
40
+ flagged += 1;
41
+ else
42
+ unflagged += 1;
43
+ if (rec.areas.length === 0) {
44
+ unlocalized += 1;
45
+ continue;
46
+ }
47
+ for (const area of rec.areas) {
48
+ let acc = byArea.get(area.key);
49
+ if (acc === undefined) {
50
+ acc = {
51
+ key: area.key,
52
+ sessionsTotal: 0,
53
+ sessionsFlagged: 0,
54
+ flaggedScores: [],
55
+ flaggedSignals: [],
56
+ };
57
+ byArea.set(area.key, acc);
58
+ }
59
+ acc.sessionsTotal += area.weight;
60
+ if (rec.flagged) {
61
+ acc.sessionsFlagged += area.weight;
62
+ acc.flaggedScores.push(rec.score_pct);
63
+ acc.flaggedSignals.push(rec.signals);
64
+ }
65
+ }
66
+ }
67
+ const rows = [];
68
+ for (const acc of byArea.values()) {
69
+ const meanScore = acc.flaggedScores.length > 0
70
+ ? acc.flaggedScores.reduce((a, b) => a + b, 0) / acc.flaggedScores.length
71
+ : 0;
72
+ rows.push({
73
+ key: acc.key,
74
+ sessions_total: acc.sessionsTotal,
75
+ sessions_flagged: acc.sessionsFlagged,
76
+ mean_score: meanScore,
77
+ top_signals: topSignalsForArea(acc.flaggedSignals),
78
+ });
79
+ }
80
+ rows.sort((a, b) => {
81
+ if (b.sessions_flagged !== a.sessions_flagged) {
82
+ return b.sessions_flagged - a.sessions_flagged;
83
+ }
84
+ return b.mean_score - a.mean_score;
85
+ });
86
+ return { rows, summary: { flagged, unflagged, unlocalized } };
87
+ }
88
+ /**
89
+ * Pick up to 3 top signals for an area from its flagged sessions' signals.
90
+ *
91
+ * For each signal spec: compute the median of its raw value across the flagged
92
+ * sessions (booleans use majority; nullable signals skip when all null). Rank
93
+ * by median value descending (higher = more struggle; `abandonment: true`
94
+ * counts as 1). Take the top 3. Ties preserve `SIGNAL_SPECS` order (stable).
95
+ * An area with no flagged sessions yields `[]`.
96
+ */
97
+ function topSignalsForArea(flaggedSignals) {
98
+ if (flaggedSignals.length === 0)
99
+ return [];
100
+ const candidates = [];
101
+ for (const spec of SIGNAL_SPECS) {
102
+ if (spec.kind === 'boolean') {
103
+ const values = flaggedSignals.map((s) => s[spec.field]);
104
+ const med = medianBoolean(values);
105
+ candidates.push({
106
+ name: spec.name,
107
+ value: med,
108
+ display: formatDisplay(spec, med),
109
+ sortKey: med ? 1 : 0,
110
+ });
111
+ continue;
112
+ }
113
+ const values = flaggedSignals
114
+ .map((s) => s[spec.field])
115
+ .filter((v) => v !== null);
116
+ if (values.length === 0)
117
+ continue; // all null → skip this signal
118
+ const med = median(values);
119
+ if (med === null)
120
+ continue; // unreachable when values.length > 0
121
+ candidates.push({
122
+ name: spec.name,
123
+ value: med,
124
+ display: formatDisplay(spec, med),
125
+ sortKey: med,
126
+ });
127
+ }
128
+ candidates.sort((a, b) => b.sortKey - a.sortKey);
129
+ return candidates.slice(0, 3).map(({ name, value, display }) => ({
130
+ name,
131
+ value,
132
+ display,
133
+ }));
134
+ }
135
+ /** Median of a non-empty numeric array. Returns null only for empty input. */
136
+ function median(values) {
137
+ const n = values.length;
138
+ if (n === 0)
139
+ return null;
140
+ const sorted = [...values].sort((a, b) => a - b);
141
+ const mid = Math.floor(n / 2);
142
+ if (n % 2 === 1)
143
+ return sorted[mid];
144
+ return (sorted[mid - 1] + sorted[mid]) / 2;
145
+ }
146
+ /** Median of booleans as strict majority: more than half true → true. */
147
+ function medianBoolean(values) {
148
+ const trues = values.filter(Boolean).length;
149
+ return trues * 2 > values.length;
150
+ }
151
+ /** Format a signal median per its kind for the `display` string. */
152
+ function formatDisplay(spec, value) {
153
+ switch (spec.kind) {
154
+ case 'count':
155
+ return `${spec.name}(${Math.round(value)})`;
156
+ case 'ratio':
157
+ return `${spec.name}(${value.toFixed(1)})`;
158
+ case 'duration': {
159
+ const ms = value;
160
+ if (ms >= 1000)
161
+ return `${spec.name}(${Math.round(ms / 1000)}s)`;
162
+ return `${spec.name}(${Math.round(ms)}ms)`;
163
+ }
164
+ case 'boolean':
165
+ return `${spec.name}(${value ? 'yes' : 'no'})`;
166
+ }
167
+ }
168
+ //# sourceMappingURL=leaderboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"leaderboard.js","sourceRoot":"","sources":["../../src/aggregate/leaderboard.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ // Stateless CLI bin entry for harnessgap. Parses args with commander, awaits
3
+ // runScan, writes result.output to stdout, and exits with result.exitCode.
4
+ // ConfigError (and any thrown error from runScan) is caught → a SHORT message
5
+ // to stderr (no stack, no transcript paths), exit 1.
6
+ //
7
+ // Constraints (§11 egress): no network imports. Imports limited to commander,
8
+ // ./pipeline.js, ./config.js, node:process, node:fs. The package.json version
9
+ // is read at runtime via fs + URL (no JSON import attribute, no node:path
10
+ // needed). No disk writes beyond what runScan performs (none).
11
+ import { Command } from 'commander';
12
+ import { readFileSync } from 'node:fs';
13
+ import process from 'node:process';
14
+ import { runScan } from './pipeline.js';
15
+ // Resolve package.json relative to this module so the version is correct
16
+ // whether run from dist/cli.js or via the installed bin symlink. URL-based
17
+ // path avoids importing node:path / node:url.
18
+ const pkgUrl = new URL('../package.json', import.meta.url);
19
+ const pkg = JSON.parse(readFileSync(pkgUrl, 'utf8'));
20
+ const program = new Command();
21
+ program
22
+ .name('harnessgap')
23
+ .description('Stateless detection-only CLI for harness gaps')
24
+ .version(pkg.version);
25
+ program
26
+ .command('scan', { isDefault: true })
27
+ .description('Scan Claude Code transcripts for harness gaps')
28
+ .option('--repo <path>', 'filter to a specific repo toplevel')
29
+ .option('--since <dur>', 'only sessions within this lookback (e.g. 30d, 12h)')
30
+ .option('--limit <n>', 'cap the number of sessions', (v) => Number.parseInt(v, 10))
31
+ .option('--json', 'emit the JSON envelope instead of a human table')
32
+ .option('--calibrate', 'emit the calibrate signal view')
33
+ .option('--bootstrap', 'force bootstrap scoring mode')
34
+ .option('--config <path>', 'path to a .harnessgap.yml config file')
35
+ .option('--claude-dir <path>', 'Claude Code config directory (contains projects/)')
36
+ .action(async (opts) => {
37
+ const scanOpts = {
38
+ repo: opts.repo,
39
+ since: opts.since,
40
+ limit: opts.limit,
41
+ json: opts.json,
42
+ calibrate: opts.calibrate,
43
+ bootstrap: opts.bootstrap,
44
+ configPath: opts.config,
45
+ claudeDir: opts.claudeDir,
46
+ };
47
+ try {
48
+ const result = await runScan(scanOpts);
49
+ // Write then exit in the flush callback so piped stdout is never
50
+ // truncated by process.exit.
51
+ process.stdout.write(result.output + '\n', () => process.exit(result.exitCode));
52
+ }
53
+ catch (e) {
54
+ // ConfigError carries a clean human message; any other thrown error is
55
+ // surfaced by message only — never the stack.
56
+ const msg = e instanceof Error ? e.message : String(e);
57
+ process.stderr.write(`error: ${msg}\n`, () => process.exit(1));
58
+ }
59
+ });
60
+ // parseAsync returns a promise; the action handles its own errors + exit, so
61
+ // this .catch is a defensive net for anything commander itself rejects (e.g.
62
+ // unknown options — commander prints its own message and exits non-zero there).
63
+ program.parseAsync(process.argv).catch((e) => {
64
+ const msg = e instanceof Error ? e.message : String(e);
65
+ process.stderr.write(`error: ${msg}\n`, () => process.exit(1));
66
+ });
67
+ //# sourceMappingURL=cli.js.map