@drakulavich/zapara 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +19 -19
- package/package.json +1 -1
- package/src/derive.ts +31 -14
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,11 @@ All notable changes to this project are documented here. The format follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.3.1] - 2026-09-19
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Streak and active minutes count your presence, not the agent's: a streak is your prompts no more than 10 minutes apart across sessions, and active minutes are the five-minute slots those streaks cover. Agent work while you are away no longer keeps a streak alive or fills the day.
|
|
12
|
+
|
|
8
13
|
## [0.3.0] - 2026-09-19
|
|
9
14
|
|
|
10
15
|
### Added
|
package/README.md
CHANGED
|
@@ -38,29 +38,29 @@ Both pictures below come from the synthetic fixture in `tests/fixtures/busy-week
|
|
|
38
38
|
|
|
39
39
|
```
|
|
40
40
|
00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 peak active
|
|
41
|
-
Mon 14/09 · · · · · · · · · ░ ░ ░ █ █ █ · · · · · ░ ░ · ░ 87
|
|
42
|
-
Tue 15/09 · · · · · · · · · · ░ ░ ░ ░ ▒ ▒ ▒ ░ · · · · · · 33
|
|
41
|
+
Mon 14/09 · · · · · · · · · ░ ░ ░ █ █ █ · · · · · ░ ░ · ░ 87 8h50
|
|
42
|
+
Tue 15/09 · · · · · · · · · · ░ ░ ░ ░ ▒ ▒ ▒ ░ · · · · · · 33 7h55
|
|
43
43
|
Wed 16/09 · · · · · · · · · · · · · · · · · · · · · · · · - 0h00
|
|
44
|
-
Thu 17/09 · · · · · · · · · · · ░ ░ · · · · · · · · · · · 14
|
|
45
|
-
Fri 18/09 · · · · · · · · · · · · · · · ▓ · · · · · · · ·
|
|
46
|
-
Sat 19/09 ░ ░ · · · · · · · · · · · · · · · · · · · · · · 24
|
|
44
|
+
Thu 17/09 · · · · · · · · · · · ░ ░ · · · · · · · · · · · 14 1h55
|
|
45
|
+
Fri 18/09 · · · · · · · · · · · · · · · ▓ · · · · · · · · 81 1h00
|
|
46
|
+
Sat 19/09 ░ ░ · · · · · · · · · · · · · · · · · · · · · · 24 1h55
|
|
47
47
|
Sun 20/09 · · · · · · · · · · · · · · · · · · · · · · · · - 0h00
|
|
48
48
|
|
|
49
49
|
░ calm ▒ warming ▓ heating █ fried
|
|
50
|
-
|
|
50
|
+
21h35 active 348 prompts 0 reports 100 decisions 5 sessions at once
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
```
|
|
54
54
|
hour index level sess prompts intr rej quest plan mode ctx-sw streak out-tok par pace sup read strk late
|
|
55
|
-
09:00 9 Calm 1 6 0 0 0 0 0 0
|
|
56
|
-
10:00 14 Calm 1 6 0 0 0 0 0 0
|
|
57
|
-
11:00 15 Calm 1
|
|
58
|
-
12:00 87 Fried 5 55 20 1 1 1 2 54
|
|
59
|
-
13:00 87 Fried 5 55 20 1 1 1 2 54
|
|
60
|
-
14:00 87 Fried 5 55 20 1 1 1 2 54
|
|
61
|
-
20:00 9 Calm 1 6 0 0 0 0 0 0
|
|
62
|
-
21:00 14 Calm 1 6 0 0 0 0 0 0
|
|
63
|
-
23:00 19 Calm 1 6 0 0 0 0 0 0
|
|
55
|
+
09:00 9 Calm 1 6 0 0 0 0 0 0 50m 600 0 4.5 0 0.1 4.2 0
|
|
56
|
+
10:00 14 Calm 1 6 0 0 0 0 0 0 110m 600 0 4.5 0 0.1 9.2 0
|
|
57
|
+
11:00 15 Calm 1 7 0 0 0 0 0 0 179m 600 0 5.3 0 0.1 10 0
|
|
58
|
+
12:00 87 Fried 5 55 20 1 1 1 2 54 235m 55.0k 25 15 30 6.9 10 0
|
|
59
|
+
13:00 87 Fried 5 55 20 1 1 1 2 54 295m 55.0k 25 15 30 6.9 10 0
|
|
60
|
+
14:00 87 Fried 5 55 20 1 1 1 2 54 355m 55.0k 25 15 30 6.9 10 0
|
|
61
|
+
20:00 9 Calm 1 6 0 0 0 0 0 0 50m 600 0 4.5 0 0.1 4.2 0
|
|
62
|
+
21:00 14 Calm 1 6 0 0 0 0 0 0 110m 600 0 4.5 0 0.1 9.2 0
|
|
63
|
+
23:00 19 Calm 1 6 0 0 0 0 0 0 50m 600 0 4.5 0 0.1 4.2 10
|
|
64
64
|
no reports today
|
|
65
65
|
```
|
|
66
66
|
|
|
@@ -142,15 +142,15 @@ A status line wants one number every thirty seconds and cannot wait half a secon
|
|
|
142
142
|
|
|
143
143
|
| Field | Meaning |
|
|
144
144
|
|---|---|
|
|
145
|
-
| `schema` | The shape of this file: `1`. A reader that sees a number it does not know shows nothing. It changes only when a field changes meaning or goes away; adding a field does not bump it. |
|
|
145
|
+
| `schema` | The shape of this file: `1`. A reader that sees a number it does not know shows nothing. It changes only when a field changes meaning or goes away; adding a field does not bump it. A field that keeps its name, unit and range but is measured differently (the presence rule of 2026-09-19 for `activeMin` and `streakMin`) does not bump it either: a reader shows the corrected number, and the change is a CHANGELOG entry. |
|
|
146
146
|
| `asOf` | When the snapshot was taken, ISO 8601 UTC: the `Day.asOf` of the base spec, the `now` of this run. A reader decides staleness from this field, never from the file's mtime. |
|
|
147
147
|
| `date` | The local calendar day the numbers describe, `YYYY-MM-DD`. |
|
|
148
148
|
| `hour` | The local hour that contains `asOf`, `0`..`23`. |
|
|
149
149
|
| `index` | That hour's load index, `0`..`100`, or `null` when the hour has no activity yet. |
|
|
150
150
|
| `level` | That hour's level, `Calm`, `Warming`, `Heating` or `Fried`, or `null` with `index`. A reader colors by this field so it never needs the thresholds. |
|
|
151
151
|
| `peak` | The day's peak index so far, or `null` on a day with no activity. |
|
|
152
|
-
| `activeMin` |
|
|
153
|
-
| `streakMin` | Minutes of the unbroken streak as of the current hour, `0` when
|
|
152
|
+
| `activeMin` | Minutes of your presence in the day so far: the 5-minute slots covered by your prompts and the gaps of at most 10 minutes between them; `0` on a day with no prompt. |
|
|
153
|
+
| `streakMin` | Minutes of the unbroken presence streak as of the current hour: your prompts no more than 10 minutes apart, across sessions; `0` when the hour has none. |
|
|
154
154
|
|
|
155
155
|
Refreshing is the reader's job, and zapara adds no hook, no timer and no daemon. A reader decodes the file strictly and treats anything that fails validation, and a missing or unreadable file, as no data: it draws nothing and counts the file as stale. When the snapshot is stale or missing it starts `zapara status` as a detached process, does not wait for it, and draws what it has, which is also how the file first comes to exist on a machine that never ran zapara. It starts at most one run per threshold and never one per render, so a file that never validates costs one run per threshold and no more; the whole contract is in [the status file spec](docs/superpowers/specs/2026-09-19-zapara-status-file-design.md), and [pult](https://github.com/drakulavich/pult) is the reader that exists, with a five-minute threshold.
|
|
156
156
|
|
|
@@ -221,7 +221,7 @@ Weights and norms live in one exported constant in `src/score.ts`, so a recalibr
|
|
|
221
221
|
| `plan` | `ExitPlanMode` tool calls in an assistant message, one per block. |
|
|
222
222
|
| `mode` | Permission-mode switches. A `permission-mode` record whose mode differs from the previous one; the first record of a session sets the baseline and repeats of the same mode count nothing. |
|
|
223
223
|
| `ctx-sw` | Context switches. Over the hour's prompts in time order, the number of consecutive pairs that came from different sessions. |
|
|
224
|
-
| `streak` | Minutes since the current
|
|
224
|
+
| `streak` | Minutes since the current presence streak began: your prompts no more than 10 minutes apart, across sessions, which may reach back before the hour. Agent activity between two prompts does not bridge a gap. |
|
|
225
225
|
| `out-tok` | Assistant output tokens, summed once per request and only for requests that produced a text block. Claude Code repeats the same usage on each content block of a response, and a request holding only tool calls is not text anyone reads. |
|
|
226
226
|
|
|
227
227
|
`bun run stats --days 14` is the tool the norms were set with. It prints the per-hour distribution of each signal over the active hours of a window (n, p50, p75, p90, max, and how many hours were zero), the top hours by reports and by human prompts, and a format-drift line comparing records seen against events the parser recognised. Run it on another machine, or after a Claude Code update, to see whether the norms and the parser still fit. It prints numbers and nothing else.
|
package/package.json
CHANGED
package/src/derive.ts
CHANGED
|
@@ -30,12 +30,20 @@ const emptyMetrics = (): Metrics => ({
|
|
|
30
30
|
decisions: 0, contextSwitches: 0, activeMin: 0, streakMin: 0, lateNight: false,
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
type Acc = { m: Metrics; sessions: Set<string>; slots: Set<number>; lastPromptSession: string | null;
|
|
33
|
+
type Acc = { m: Metrics; sessions: Set<string>; slots: Set<number>; lastPromptSession: string | null; lastPrompt: { ts: number; streakStart: number } | null };
|
|
34
34
|
|
|
35
35
|
// Walks the sorted, look-back-filtered events once, keyed by "date|hour",
|
|
36
|
-
// tracking the running
|
|
37
|
-
// accumulating each bucket's raw counts.
|
|
38
|
-
//
|
|
36
|
+
// tracking the running presence streak (which may start before startMs) and
|
|
37
|
+
// accumulating each bucket's raw counts.
|
|
38
|
+
//
|
|
39
|
+
// Presence is the human's: both the streak and the covered slots are built from
|
|
40
|
+
// `prompt` events alone, because both answer "is it time to rest?". Agents that
|
|
41
|
+
// work on while the human is away must not keep a streak alive or fill the day,
|
|
42
|
+
// so `activity` is left with session liveness and nothing else.
|
|
43
|
+
//
|
|
44
|
+
// Prompts before startMs update presence only: they are never counted in a
|
|
45
|
+
// bucket, and neither are the slots they cover before startMs, but a span from
|
|
46
|
+
// such a prompt into the window still covers the window's first slots.
|
|
39
47
|
function foldEvents(sorted: Event[], startMs: number): Map<string, Acc> {
|
|
40
48
|
const acc = new Map<string, Acc>(); // key "date|hour"
|
|
41
49
|
const key = (ts: number): string => {
|
|
@@ -44,29 +52,38 @@ function foldEvents(sorted: Event[], startMs: number): Map<string, Acc> {
|
|
|
44
52
|
};
|
|
45
53
|
const get = (k: string): Acc => {
|
|
46
54
|
let a = acc.get(k);
|
|
47
|
-
if (!a) { a = { m: emptyMetrics(), sessions: new Set(), slots: new Set(), lastPromptSession: null,
|
|
55
|
+
if (!a) { a = { m: emptyMetrics(), sessions: new Set(), slots: new Set(), lastPromptSession: null, lastPrompt: null }; acc.set(k, a); }
|
|
48
56
|
return a;
|
|
49
57
|
};
|
|
50
58
|
|
|
51
|
-
let
|
|
52
|
-
let streakStart
|
|
59
|
+
let prevPromptTs: number | null = null;
|
|
60
|
+
let streakStart = 0; // always set by the first prompt, which starts a streak
|
|
53
61
|
for (const e of sorted) {
|
|
54
|
-
if (e.kind === "
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
if (e.kind === "prompt") {
|
|
63
|
+
const slot = Math.floor(e.ts / SLOT_MS);
|
|
64
|
+
// A prompt covers its own slot. Inside a streak the human sat through the
|
|
65
|
+
// gap too, so the pair also covers every slot between them; a prompt that
|
|
66
|
+
// starts a streak covers nothing behind it.
|
|
67
|
+
let from = slot;
|
|
68
|
+
if (prevPromptTs !== null && e.ts - prevPromptTs <= GAP_MS) from = Math.floor(prevPromptTs / SLOT_MS);
|
|
69
|
+
else streakStart = e.ts;
|
|
70
|
+
for (let s = from; s <= slot; s++) {
|
|
71
|
+
const slotStart = s * SLOT_MS;
|
|
72
|
+
if (slotStart >= startMs) get(key(slotStart)).slots.add(s); // a slot belongs to the bucket of its start
|
|
73
|
+
}
|
|
74
|
+
prevPromptTs = e.ts;
|
|
57
75
|
}
|
|
58
|
-
if (e.ts < startMs) continue; // look-back:
|
|
76
|
+
if (e.ts < startMs) continue; // look-back: presence bookkeeping only
|
|
59
77
|
const a = get(key(e.ts));
|
|
60
78
|
switch (e.kind) {
|
|
61
79
|
case "activity":
|
|
62
80
|
a.sessions.add(e.sessionId);
|
|
63
|
-
a.slots.add(Math.floor(e.ts / SLOT_MS));
|
|
64
|
-
a.lastActivity = { ts: e.ts, streakStart: streakStart! };
|
|
65
81
|
break;
|
|
66
82
|
case "prompt":
|
|
67
83
|
a.m.prompts++;
|
|
68
84
|
if (a.lastPromptSession !== null && a.lastPromptSession !== e.sessionId) a.m.contextSwitches++;
|
|
69
85
|
a.lastPromptSession = e.sessionId;
|
|
86
|
+
a.lastPrompt = { ts: e.ts, streakStart };
|
|
70
87
|
break;
|
|
71
88
|
case "report": a.m.reports++; break;
|
|
72
89
|
case "output": a.m.outputTokens += e.tokens ?? 0; break;
|
|
@@ -90,7 +107,7 @@ function buildDay(date: string, acc: Map<string, Acc>): Day {
|
|
|
90
107
|
if (a) {
|
|
91
108
|
m.sessions = a.sessions.size;
|
|
92
109
|
m.activeMin = a.slots.size * 5;
|
|
93
|
-
m.streakMin = a.
|
|
110
|
+
m.streakMin = a.lastPrompt ? Math.round((a.lastPrompt.ts - a.lastPrompt.streakStart) / 60000) : 0;
|
|
94
111
|
}
|
|
95
112
|
m.decisions = m.interrupts + m.rejects + m.questions + m.plans + m.modeSwitches;
|
|
96
113
|
// lateNight is a property of the hour label, so it is set on every bucket,
|