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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dmitry Teryaev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # harnessgap
2
+
3
+ A **stateless, detection-only** CLI that reads Claude Code transcript logs and
4
+ produces a *struggle leaderboard* — the areas of a repo where Claude Code
5
+ sessions show the deterministic signals of friction (rereads, failure streaks,
6
+ oscillating edits, abandonment, etc.).
7
+
8
+ This is **Slice 1**: it writes nothing, installs nothing, persists nothing. It
9
+ only prints a leaderboard to stdout. Diagnosis, synthesis, routing, and
10
+ measurement are deferred to later slices.
11
+
12
+ > **Full manual:** [docs/CONSUMER_GUIDE.md](docs/CONSUMER_GUIDE.md) — output formats, scoring modes, calibration, FAQ. **Internals:** [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
13
+
14
+ ## Install
15
+
16
+ ```
17
+ npx harnessgap
18
+ ```
19
+
20
+ Or build from source:
21
+
22
+ ```
23
+ npm install
24
+ npm run build
25
+ node dist/cli.js scan
26
+ ```
27
+
28
+ Requires Node >= 22.12.
29
+
30
+ ## Usage
31
+
32
+ ```
33
+ harnessgap scan [options]
34
+ ```
35
+
36
+ `scan` is the default command. It walks Claude Code transcripts under
37
+ `~/.claude/projects/`, filters to a repo, runs the detector, and prints a
38
+ leaderboard of struggle areas.
39
+
40
+ ### Flags
41
+
42
+ | Flag | Description |
43
+ | --- | --- |
44
+ | `--repo <path>` | Filter to sessions whose resolved repo toplevel matches this path. Defaults to the git toplevel of the current working directory. |
45
+ | `--since <dur>` | Only sessions started within this lookback, e.g. `30d`, `12h`, `5m`, `10s`. Default: all sessions. |
46
+ | `--limit <n>` | Cap the number of sessions scanned. Useful for fast iteration. |
47
+ | `--json` | Emit the JSON envelope (for piping) instead of the human-readable leaderboard table. |
48
+ | `--calibrate` | Print per-signal distributions (min / p50 / p90 / max) plus active thresholds and scoring mode. Aggregate statistics only — no per-session detail. |
49
+ | `--bootstrap` | Force bootstrap (absolute-threshold) scoring mode instead of percentile. |
50
+ | `--config <path>` | Path to a `.harnessgap.yml` config file. Default: looks for `.harnessgap.yml` in the cwd. |
51
+ | `--claude-dir <path>` | Claude Code config directory (contains `projects/`). Default: `~/.claude`. |
52
+ | `--version` | Print the harnessgap version and exit. |
53
+ | `--help` | Print help and exit. |
54
+
55
+ ## Configuration (`.harnessgap.yml`)
56
+
57
+ Optional. `scan` runs with built-in defaults if no file is present. The file is
58
+ a YAML object with two top-level keys — `detector` and `areas`. Anything else is
59
+ rejected. Deep-merged over the defaults (arrays replace, they do not concatenate).
60
+
61
+ ```yaml
62
+ detector:
63
+ thresholds_as: percentile # percentile (default) | absolute
64
+ flag_pct: 90 # percentile mode: flag the top (100 - flag_pct)% of composites
65
+ bootstrap_session_floor: 30 # below this session count, bootstrap mode is automatic
66
+ bootstrap_flag_pct: 70 # bootstrap mode: flag if composite >= this OR >= 2 signals trip
67
+ reread_threshold: 5 # a file read count at/above this counts as a reread
68
+ correction_window_ms: 120000 # window after an edit in which a follow-up edit counts as a correction
69
+ signal_weights: # composite weights per signal
70
+ explore_ratio: 1.0
71
+ reread: 1.0
72
+ failure_streak: 1.0
73
+ corrections: 1.0
74
+ abandonment: 0.5 # low until task fingerprinting lands
75
+ oscillation: 1.2
76
+ wall_clock_per_line: 1.0
77
+ bootstrap_thresholds: # conservative absolute priors used in bootstrap mode
78
+ explore_ratio: 10
79
+ reread: 5
80
+ failure_streak: 3
81
+ corrections: 2
82
+ abandonment: true
83
+ oscillation: 2
84
+ wall_clock_per_line_ms: 300000
85
+
86
+ areas:
87
+ ignore: [node_modules, build, target, dist, .git, .next, vendor] # path prefixes excluded from area clustering
88
+ min_weight: 0.40 # minimum cumulative touch weight for an area to appear
89
+ min_depth: 2 # minimum path depth for an area key
90
+ touch_weights: { edit: 3, read: 2, exec: 1 } # weight of each touch kind
91
+ tail_fraction: 0.25 # fraction of a session's events that defines the abandonment tail
92
+ explore_ratio_min: 0.8 # abandonment explore-ratio gate
93
+ suppress_abandonment_when_no_exec: true
94
+ test_cmd_patterns: [test, spec, pytest, "npm test", "npm run test", make, "cargo test", "go test", jest, vitest]
95
+ ```
96
+
97
+ Keys not shown here (`docs_dirs`, `synthesizer`, `router`, `tasks`, `repo`) are
98
+ **not** part of Slice 1 and will be rejected.
99
+
100
+ ## Success criterion
101
+
102
+ Slice 1 is validated by a **manual dogfood gate**, not an automated test. On a
103
+ real repo with rich Claude Code session history, the user prepares, in advance:
104
+
105
+ - **>= 5 areas they recall as struggle**, and
106
+ - **>= 5 areas they recall as non-struggle.**
107
+
108
+ `harnessgap scan` must then satisfy all three against its leaderboard:
109
+
110
+ - **Precision** — of the tool's top 5 flagged areas, >= 3 are in the user's
111
+ struggle set (>= 60%).
112
+ - **Recall** — of the user's >= 5 struggle areas, >= 3 are flagged (>= 60%).
113
+ - **No false positives in the top 5** — none of the user's non-struggle areas
114
+ appear in the top 5 flagged.
115
+
116
+ The labeled fixture corpus and snapshot test (see `test/corpus.test.ts`,
117
+ `test/snapshot.test.ts`) serve as the automated regression proxy for this gate.
118
+
119
+ ## Privacy
120
+
121
+ harnessgap is built to run offline on private transcripts. Five guarantees:
122
+
123
+ 1. **No network.** No `fetch` / `http` / `https` / `net` / `undici` imports and
124
+ no `fetch()` calls anywhere in `src/`. Transcripts never leave the machine.
125
+ Enforced by `test/egress.test.ts`, which scans every `src/**/*.ts` file for
126
+ forbidden network imports and fetch calls, and runs in CI.
127
+ 2. **No disk writes.** harnessgap writes nothing to disk. It reads transcripts
128
+ and prints to stdout. (OS-level page cache/swap are out of scope and common
129
+ to any process that reads files.)
130
+ 3. **Pattern-catalog scrubbing.** Secrets are scrubbed in the adapter, before
131
+ events enter the pipeline, using a fixed pattern catalog (API keys, bearer
132
+ tokens, private keys, connection strings, etc.).
133
+ 4. **No raw prose in output.** Only derived signal values and integer warning
134
+ counts are emitted. Raw message text, commands, and transcript line content
135
+ never appear in any output path (human table, `--json`, `--calibrate`,
136
+ warnings).
137
+ 5. **Sandboxed git.** `git` is invoked via `execFile` with no shell, so no
138
+ command lands in shell history. Symlinks in transcript directories are
139
+ rejected.
140
+
141
+ ## Dependency egress audit
142
+
143
+ Runtime dependencies are exactly two, both no-egress (neither performs network
144
+ I/O):
145
+
146
+ - [`commander`](https://www.npmjs.com/package/commander) — CLI argument parsing.
147
+ - [`yaml`](https://www.npmjs.com/package/yaml) — `.harnessgap.yml` parsing.
148
+
149
+ Dev dependencies (`typescript`, `vitest`, `tsx`, `@types/node`) are never shipped
150
+ and excluded from the published `files` set (`["dist"]`).
151
+
152
+ The audit is locked by `test/packaging.test.ts`, which runs
153
+ `npm ls --all --omit=dev --json` (via `execFile`, no shell) and asserts the
154
+ runtime `dependencies` object has exactly the keys `commander` and `yaml`. The
155
+ `test/egress.test.ts` gate additionally asserts no `src/` file imports a network
156
+ module or calls `fetch()`. Both run in CI.
157
+
158
+ ## Spec
159
+
160
+ Full design: [`docs/superpowers/specs/active/2026-07-12-harnessgap-detection-slice-design.md`](docs/superpowers/specs/active/2026-07-12-harnessgap-detection-slice-design.md).
@@ -0,0 +1,14 @@
1
+ import type { Correction, ToolKind } from '../types.js';
2
+ /**
3
+ * Detect whether a user message is a course-correction of the immediately
4
+ * preceding tool call. Returns `{matched, shape}` — never the raw text.
5
+ *
6
+ * Shape catalog (first match wins): negation → undo → redirect → retry-different.
7
+ * Negation is start-anchored (with a word boundary); the other three use
8
+ * substring `includes`. Messages shorter than 3 chars are not matched.
9
+ * `prevToolCall` is part of the interface contract but not used by this
10
+ * content-based heuristic.
11
+ */
12
+ export declare function detectCorrection(prevToolCall: {
13
+ tool: ToolKind;
14
+ } | null, userText: string): Correction;
@@ -0,0 +1,68 @@
1
+ // Content-based correction detector — a heuristic priors catalog that
2
+ // classifies whether a user message is a course-correction of the preceding
3
+ // tool call. Pure function: reads userText internally, emits ONLY
4
+ // {matched, shape}. Never returns the raw text. Spec §12.5 open question
5
+ // (whether interrupts are recorded in transcripts); this is the
6
+ // content-based fallback.
7
+ // Negation tokens are start-anchored: checked against the start of the trimmed
8
+ // lowercased message, with a word boundary so "no" does not match "note".
9
+ const NEGATION_TOKENS = [
10
+ 'no',
11
+ 'no,',
12
+ "don't",
13
+ 'do not',
14
+ 'stop',
15
+ 'wait',
16
+ 'hold on',
17
+ "that's wrong",
18
+ 'not that',
19
+ ];
20
+ // undo / redirect / retry-different use substring includes (case-insensitive).
21
+ const UNDO_TOKENS = ['undo', 'revert', 'roll back', 'put it back'];
22
+ const REDIRECT_TOKENS = ['instead', 'actually', 'rather', 'try the other'];
23
+ const RETRY_DIFFERENT_TOKENS = ['try again', 'differently', 'another approach'];
24
+ const NOT_MATCHED = { matched: false, shape: null };
25
+ function isLetter(code) {
26
+ return code >= 97 && code <= 122; // a-z (text is already lowercased)
27
+ }
28
+ /**
29
+ * Test whether `text` starts with `token` followed by a word boundary
30
+ * (non-letter or end-of-string). Prevents "no" from matching "note".
31
+ */
32
+ function startsWithToken(text, token) {
33
+ if (!text.startsWith(token))
34
+ return false;
35
+ if (text.length === token.length)
36
+ return true;
37
+ return !isLetter(text.charCodeAt(token.length));
38
+ }
39
+ /**
40
+ * Detect whether a user message is a course-correction of the immediately
41
+ * preceding tool call. Returns `{matched, shape}` — never the raw text.
42
+ *
43
+ * Shape catalog (first match wins): negation → undo → redirect → retry-different.
44
+ * Negation is start-anchored (with a word boundary); the other three use
45
+ * substring `includes`. Messages shorter than 3 chars are not matched.
46
+ * `prevToolCall` is part of the interface contract but not used by this
47
+ * content-based heuristic.
48
+ */
49
+ export function detectCorrection(prevToolCall, userText) {
50
+ void prevToolCall; // interface contract; not used by this heuristic
51
+ const text = userText.trim().toLowerCase().replace(/’/g, "'");
52
+ if (text.length < 3)
53
+ return NOT_MATCHED;
54
+ if (NEGATION_TOKENS.some((t) => startsWithToken(text, t))) {
55
+ return { matched: true, shape: 'negation' };
56
+ }
57
+ if (UNDO_TOKENS.some((t) => text.includes(t))) {
58
+ return { matched: true, shape: 'undo' };
59
+ }
60
+ if (REDIRECT_TOKENS.some((t) => text.includes(t))) {
61
+ return { matched: true, shape: 'redirect' };
62
+ }
63
+ if (RETRY_DIFFERENT_TOKENS.some((t) => text.includes(t))) {
64
+ return { matched: true, shape: 'retry-different' };
65
+ }
66
+ return NOT_MATCHED;
67
+ }
68
+ //# sourceMappingURL=correction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"correction.js","sourceRoot":"","sources":["../../src/adapter/correction.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,4EAA4E;AAC5E,kEAAkE;AAClE,yEAAyE;AACzE,gEAAgE;AAChE,0BAA0B;AAI1B,+EAA+E;AAC/E,0EAA0E;AAC1E,MAAM,eAAe,GAAG;IACtB,IAAI;IACJ,KAAK;IACL,OAAO;IACP,QAAQ;IACR,MAAM;IACN,MAAM;IACN,SAAS;IACT,cAAc;IACd,UAAU;CACX,CAAC;AAEF,+EAA+E;AAC/E,MAAM,WAAW,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,CAAC,CAAC;AACnE,MAAM,eAAe,GAAG,CAAC,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,eAAe,CAAC,CAAC;AAC3E,MAAM,sBAAsB,GAAG,CAAC,WAAW,EAAE,aAAa,EAAE,kBAAkB,CAAC,CAAC;AAEhF,MAAM,WAAW,GAAe,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAEhE,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,IAAI,IAAI,EAAE,IAAI,IAAI,IAAI,GAAG,CAAC,CAAC,mCAAmC;AACvE,CAAC;AAED;;;GAGG;AACH,SAAS,eAAe,CAAC,IAAY,EAAE,KAAa;IAClD,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC1C,IAAI,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAC9C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;AAClD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAC9B,YAAuC,EACvC,QAAgB;IAEhB,KAAK,YAAY,CAAC,CAAC,iDAAiD;IACpE,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC9D,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,WAAW,CAAC;IAExC,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;IAC9C,CAAC;IACD,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAC1C,CAAC;IACD,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAClD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;IAC9C,CAAC;IACD,IAAI,sBAAsB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACzD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC;IACrD,CAAC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { NormalizedEvent, ToolKind } from '../types.js';
2
+ /**
3
+ * Convert one parsed Claude Code JSONL record into a NormalizedEvent, or return
4
+ * null when the record carries no usable event (system entries, ai-title, etc.).
5
+ *
6
+ * Mapping:
7
+ * - assistant tool_use → tool_call (tool + digest from input; ok=true)
8
+ * - user-record tool_result → tool_call (tool=null; ok=!is_error; interrupted)
9
+ * - user text → user_msg (correction via detectCorrection; no raw text)
10
+ * - assistant text → assistant_msg
11
+ * - anything else → null
12
+ *
13
+ * `ctx.prevToolCall` is passed to detectCorrection for user messages; it is NOT
14
+ * mutated here (the stream layer updates it between records). No raw prose is
15
+ * emitted: userText is consumed by detectCorrection and discarded.
16
+ */
17
+ export declare function normalizeRecord(raw: unknown, ctx: {
18
+ prevToolCall: {
19
+ tool: ToolKind;
20
+ } | null;
21
+ }): NormalizedEvent | null;
@@ -0,0 +1,261 @@
1
+ // Claude Code transcript-record normalizer. Pure function: takes one already-
2
+ // parsed JSONL record object and returns a NormalizedEvent (or null when the
3
+ // record carries no usable event). No I/O, no network. Scrubbing, caps, and the
4
+ // correction flag are applied here so downstream stages never see raw prose.
5
+ //
6
+ // Field-name constants below were confirmed by inspecting real transcripts at
7
+ // ~/.claude/projects/-Users-dmitry-Desktop-CursorProjects-harnessgap/*.jsonl
8
+ // (one JSON object per line). Behavioral contract is pinned to the task-5 brief
9
+ // regardless of upstream schema drift.
10
+ import { scrubCmd, scrubFiles, scrubQuery } from './scrub.js';
11
+ import { detectCorrection } from './correction.js';
12
+ import { mapToolKind } from './taxonomy.js';
13
+ // --- Confirmed Claude Code transcript field paths (encoded as constants) ---
14
+ const RECORD_TYPE = {
15
+ USER: 'user',
16
+ ASSISTANT: 'assistant',
17
+ };
18
+ const FIELD = {
19
+ TYPE: 'type',
20
+ MESSAGE: 'message',
21
+ CONTENT: 'content',
22
+ TIMESTAMP: 'timestamp',
23
+ DURATION_MS: 'duration_ms',
24
+ };
25
+ const CONTENT_TYPE = {
26
+ TOOL_USE: 'tool_use',
27
+ TEXT: 'text',
28
+ TOOL_RESULT: 'tool_result',
29
+ };
30
+ // Tool-use input field names (confirmed: Bash.command, Read.file_path,
31
+ // Edit.{file_path,old_string,new_string}, Write.{file_path,content},
32
+ // Grep/Glob.pattern, LS.path).
33
+ const TOOL_INPUT = {
34
+ COMMAND: 'command',
35
+ FILE_PATH: 'file_path',
36
+ PATH: 'path',
37
+ FILES: 'files',
38
+ PATTERN: 'pattern',
39
+ OLD_STRING: 'old_string',
40
+ NEW_STRING: 'new_string',
41
+ CONTENT: 'content',
42
+ };
43
+ // tool_result content-item field names (confirmed: is_error present on real
44
+ // records; interrupted absent in sample but part of the contract — default false).
45
+ const RESULT_FIELD = {
46
+ IS_ERROR: 'is_error',
47
+ INTERRUPTED: 'interrupted',
48
+ };
49
+ // --- Helpers (module-private; only normalizeRecord is exported) ---
50
+ function emptyDigest() {
51
+ return { files: [], cmd: null, query: null, lines_changed: null };
52
+ }
53
+ function findContentItem(content, type) {
54
+ if (!Array.isArray(content))
55
+ return null;
56
+ for (const item of content) {
57
+ if (item && typeof item === 'object' && item.type === type) {
58
+ return item;
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+ /** Extract user/assistant prose text from a message content field. */
64
+ function extractText(content) {
65
+ if (typeof content === 'string')
66
+ return content;
67
+ if (Array.isArray(content)) {
68
+ for (const item of content) {
69
+ if (typeof item === 'string')
70
+ return item;
71
+ if (item && typeof item === 'object') {
72
+ const ci = item;
73
+ if (ci.type === CONTENT_TYPE.TEXT && typeof ci.text === 'string')
74
+ return ci.text;
75
+ }
76
+ }
77
+ }
78
+ return null;
79
+ }
80
+ /**
81
+ * Extract file paths from a tool-use input. If `input.files` is a non-empty
82
+ * array of strings, use it (multi-file case); otherwise collect the single
83
+ * `file_path` / `path` fields present.
84
+ */
85
+ function extractPaths(input) {
86
+ const filesRaw = input[TOOL_INPUT.FILES];
87
+ if (Array.isArray(filesRaw)) {
88
+ const files = filesRaw.filter((f) => typeof f === 'string');
89
+ if (files.length > 0)
90
+ return files;
91
+ }
92
+ const paths = [];
93
+ const fp = input[TOOL_INPUT.FILE_PATH];
94
+ if (typeof fp === 'string')
95
+ paths.push(fp);
96
+ const p = input[TOOL_INPUT.PATH];
97
+ if (typeof p === 'string')
98
+ paths.push(p);
99
+ return paths;
100
+ }
101
+ /**
102
+ * Count lines in the "new content" of an edit, as a proxy for the amount of
103
+ * code touched (feeds explore_ratio and wall_clock_per_line). For Edit →
104
+ * new_string; for Write/NotebookEdit → content. Counts `\n`-separated lines;
105
+ * returns 0 if the field is absent or empty. A consistent "amount of new code"
106
+ * proxy is what matters downstream.
107
+ */
108
+ function countNewLines(input) {
109
+ const ns = input[TOOL_INPUT.NEW_STRING];
110
+ const s = typeof ns === 'string'
111
+ ? ns
112
+ : typeof input[TOOL_INPUT.CONTENT] === 'string'
113
+ ? input[TOOL_INPUT.CONTENT]
114
+ : null;
115
+ if (s === null || s === '')
116
+ return 0;
117
+ return s.split('\n').length;
118
+ }
119
+ /** Build the InputDigest for a tool_use event based on its ToolKind. */
120
+ function digestForTool(tool, input) {
121
+ switch (tool) {
122
+ case 'exec': {
123
+ const cmd = input[TOOL_INPUT.COMMAND];
124
+ return {
125
+ files: [],
126
+ cmd: scrubCmd(typeof cmd === 'string' ? cmd : ''),
127
+ query: null,
128
+ lines_changed: null,
129
+ };
130
+ }
131
+ case 'read':
132
+ case 'list':
133
+ return {
134
+ files: scrubFiles(extractPaths(input)),
135
+ cmd: null,
136
+ query: null,
137
+ lines_changed: null,
138
+ };
139
+ case 'edit':
140
+ return {
141
+ files: scrubFiles(extractPaths(input)),
142
+ cmd: null,
143
+ query: null,
144
+ lines_changed: countNewLines(input),
145
+ };
146
+ case 'search': {
147
+ const pattern = input[TOOL_INPUT.PATTERN];
148
+ return {
149
+ files: [],
150
+ cmd: null,
151
+ query: scrubQuery(typeof pattern === 'string' ? pattern : ''),
152
+ lines_changed: null,
153
+ };
154
+ }
155
+ case 'other':
156
+ default:
157
+ return emptyDigest();
158
+ }
159
+ }
160
+ function isRecord(raw) {
161
+ return raw !== null && typeof raw === 'object';
162
+ }
163
+ // --- Public API ---
164
+ /**
165
+ * Convert one parsed Claude Code JSONL record into a NormalizedEvent, or return
166
+ * null when the record carries no usable event (system entries, ai-title, etc.).
167
+ *
168
+ * Mapping:
169
+ * - assistant tool_use → tool_call (tool + digest from input; ok=true)
170
+ * - user-record tool_result → tool_call (tool=null; ok=!is_error; interrupted)
171
+ * - user text → user_msg (correction via detectCorrection; no raw text)
172
+ * - assistant text → assistant_msg
173
+ * - anything else → null
174
+ *
175
+ * `ctx.prevToolCall` is passed to detectCorrection for user messages; it is NOT
176
+ * mutated here (the stream layer updates it between records). No raw prose is
177
+ * emitted: userText is consumed by detectCorrection and discarded.
178
+ */
179
+ export function normalizeRecord(raw, ctx) {
180
+ if (!isRecord(raw))
181
+ return null;
182
+ const rec = raw;
183
+ const t = typeof rec[FIELD.TIMESTAMP] === 'string' ? rec[FIELD.TIMESTAMP] : '';
184
+ const duration_ms = typeof rec[FIELD.DURATION_MS] === 'number' ? rec[FIELD.DURATION_MS] : 0;
185
+ const msg = rec[FIELD.MESSAGE];
186
+ // --- User record: may carry a tool_result (nested) or plain text ---
187
+ if (rec[FIELD.TYPE] === RECORD_TYPE.USER && msg && typeof msg === 'object') {
188
+ const content = msg[FIELD.CONTENT];
189
+ // tool_result nested in a user message → tool_call result event.
190
+ const toolResult = findContentItem(content, CONTENT_TYPE.TOOL_RESULT);
191
+ if (toolResult) {
192
+ return {
193
+ t,
194
+ kind: 'tool_call',
195
+ tool: null, // result records carry no tool name
196
+ input_digest: emptyDigest(),
197
+ ok: toolResult[RESULT_FIELD.IS_ERROR] !== true,
198
+ interrupted: toolResult[RESULT_FIELD.INTERRUPTED] === true,
199
+ duration_ms,
200
+ correction: null,
201
+ };
202
+ }
203
+ // Plain text → user_msg (run correction detection; discard the text).
204
+ const userText = extractText(content);
205
+ if (userText !== null) {
206
+ const correction = detectCorrection(ctx.prevToolCall, userText);
207
+ return {
208
+ t,
209
+ kind: 'user_msg',
210
+ tool: null,
211
+ input_digest: emptyDigest(),
212
+ ok: true,
213
+ interrupted: false,
214
+ duration_ms,
215
+ correction,
216
+ };
217
+ }
218
+ return null;
219
+ }
220
+ // --- Assistant record: tool_use or text ---
221
+ if (rec[FIELD.TYPE] === RECORD_TYPE.ASSISTANT && msg && typeof msg === 'object') {
222
+ const content = msg[FIELD.CONTENT];
223
+ // tool_use → tool_call event with digest extracted from input.
224
+ const toolUse = findContentItem(content, CONTENT_TYPE.TOOL_USE);
225
+ if (toolUse) {
226
+ const name = typeof toolUse.name === 'string' ? toolUse.name : '';
227
+ const input = toolUse.input && typeof toolUse.input === 'object'
228
+ ? toolUse.input
229
+ : {};
230
+ const tool = mapToolKind(name);
231
+ return {
232
+ t,
233
+ kind: 'tool_call',
234
+ tool,
235
+ input_digest: digestForTool(tool, input),
236
+ ok: true, // ok is determined by the later tool_result, not the call
237
+ interrupted: false,
238
+ duration_ms,
239
+ correction: null,
240
+ };
241
+ }
242
+ // Text only → assistant_msg.
243
+ const text = extractText(content);
244
+ if (text !== null) {
245
+ return {
246
+ t,
247
+ kind: 'assistant_msg',
248
+ tool: null,
249
+ input_digest: emptyDigest(),
250
+ ok: true,
251
+ interrupted: false,
252
+ duration_ms,
253
+ correction: null,
254
+ };
255
+ }
256
+ return null;
257
+ }
258
+ // Unknown record type (ai-title, summary, system entry, etc.) → skip.
259
+ return null;
260
+ }
261
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/adapter/parse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Scrub an exec command string: apply the full catalog, then truncate to 512
3
+ * chars (no marker). Replacements use the fixed sentinel `***REDACTED***`.
4
+ */
5
+ export declare function scrubCmd(cmd: string): string;
6
+ /**
7
+ * Scrub a search/query string: same catalog and length cap as `scrubCmd`.
8
+ */
9
+ export declare function scrubQuery(q: string): string;
10
+ /**
11
+ * Scrub a list of file paths: replace any path matching a credential-file glob
12
+ * with the sentinel, leave others unchanged, then drop entries beyond 50 (no
13
+ * marker). Only the credential-file rule applies here — not the full catalog.
14
+ */
15
+ export declare function scrubFiles(files: string[]): string[];