@drakulavich/zapara 0.7.1 → 0.7.2

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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ All notable changes to this project are documented here. The format follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.7.2] - 2026-09-26
9
+
10
+ ### Changed
11
+ - Transcripts are read sixteen at a time instead of one by one. On a machine
12
+ with 1.6 GB of transcripts in a 14-day window, reading took 0.9 s instead
13
+ of 5.4 s.
14
+ - Transcript lines older than the window's three-hour look-back are skipped
15
+ before parsing, except assistant replies, whose request ids dedupe output
16
+ tokens. A long session's file holds its whole history, and on one machine
17
+ 59% of what a 14-day window read was such history. Results are unchanged.
18
+ Together with parallel reads, `zapara card --json` there went from 5.6 s to
19
+ about 2 s.
20
+
8
21
  ## [0.7.1] - 2026-09-25
9
22
 
10
23
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakulavich/zapara",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "Cognitive load index for people driving Claude Code, computed locally from transcripts",
5
5
  "license": "MIT",
6
6
  "author": "Anton Yakutovich",
package/src/analyze.ts CHANGED
@@ -1,11 +1,12 @@
1
- import { compareStrings, derive } from "./derive.ts";
1
+ import { compareStrings, derive, windowBounds } from "./derive.ts";
2
2
  import { parseTranscript } from "./parse.ts";
3
3
  import type { Day, Transcript, Window } from "./types.ts";
4
4
 
5
5
  // Sorted by path so file discovery order cannot change a result.
6
6
  export function analyze(transcripts: Transcript[], w: Window): Day[] {
7
+ const { cutoffMs } = windowBounds(w);
7
8
  const events = [...transcripts]
8
9
  .sort((a, b) => compareStrings(a.path, b.path))
9
- .flatMap((t) => parseTranscript(t.text));
10
+ .flatMap((t) => parseTranscript(t.text, cutoffMs));
10
11
  return derive(events, w);
11
12
  }
package/src/parse.ts CHANGED
@@ -28,7 +28,26 @@ const firstText = (content: unknown): string | null => {
28
28
  return null;
29
29
  };
30
30
 
31
- export function parseTranscript(text: string): Event[] {
31
+ // A record older than the look-back yields only events derive() drops, so it is
32
+ // not parsed at all. Every "timestamp" in the line must be old, since a nested
33
+ // one can differ from the record's own. The skip waits for a first timestamped
34
+ // record, which the mode switches before it attach to; lines naming the two
35
+ // tools whose result the human writes are always parsed, for their ids.
36
+ // One pass over the line: it costs a fifth of JSON.parse.
37
+ const SKIP_SCAN = new RegExp(`"timestamp":"([^"]{20,40})"|${QUESTION_TOOL}|${PLAN_TOOL}`, "g");
38
+ function olderThan(line: string, cutoffMs: number): boolean {
39
+ // Parsing an assistant record can update seenRequestIds even when its events
40
+ // are outside the window. Preserve that deduplication state across the skip.
41
+ if (line.includes('"requestId"')) return false;
42
+ let any = false;
43
+ for (const m of line.matchAll(SKIP_SCAN)) {
44
+ if (m[1] === undefined || !(Date.parse(m[1]) < cutoffMs)) return false;
45
+ any = true;
46
+ }
47
+ return any;
48
+ }
49
+
50
+ export function parseTranscript(text: string, cutoffMs = -Infinity): Event[] {
32
51
  const events: Event[] = [];
33
52
  let lastTs: number | null = null;
34
53
  let lastMode: string | null = null;
@@ -42,6 +61,7 @@ export function parseTranscript(text: string): Event[] {
42
61
 
43
62
  for (const line of text.split("\n")) {
44
63
  if (line.trim() === "") continue;
64
+ if (lastTs !== null && lastTs < cutoffMs && olderThan(line, cutoffMs)) continue;
45
65
  let rec: unknown;
46
66
  try { rec = JSON.parse(line); } catch { continue; }
47
67
  if (!isObj(rec)) continue;
package/src/report.ts CHANGED
@@ -6,12 +6,23 @@ import type { Day, Transcript } from "./types.ts";
6
6
 
7
7
  export type ReportOptions = { projects: string; to: string; days: number; now?: Date };
8
8
 
9
+ // Reading one file at a time left the disk idle between files: 1.6 GB took 5.4 s
10
+ // sequentially and 0.9 s in parallel. The cap keeps open files well under the limit.
11
+ const READERS = 16;
12
+
9
13
  export async function report(o: ReportOptions): Promise<Day[]> {
10
14
  const window = { to: o.to, days: o.days, now: o.now };
11
15
  const paths = await scan(o.projects, windowBounds(window).cutoffMs);
16
+ const texts: (string | null)[] = new Array(paths.length).fill(null);
17
+ let next = 0;
18
+ const reader = async (): Promise<void> => {
19
+ while (next < paths.length) {
20
+ const i = next++;
21
+ try { texts[i] = await readFile(paths[i]!, "utf8"); } catch { /* vanished or unreadable: skip */ }
22
+ }
23
+ };
24
+ await Promise.all(Array.from({ length: Math.min(READERS, paths.length) }, reader));
12
25
  const transcripts: Transcript[] = [];
13
- for (const path of paths) {
14
- try { transcripts.push({ path, text: await readFile(path, "utf8") }); } catch { /* vanished or unreadable: skip */ }
15
- }
26
+ paths.forEach((path, i) => { if (texts[i] !== null) transcripts.push({ path, text: texts[i]! }); });
16
27
  return analyze(transcripts, window);
17
28
  }