@adrrr/tarmac 0.9.0 → 0.10.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.
package/README.md CHANGED
@@ -13,7 +13,7 @@ documented surfaces only, never an internal format.
13
13
  <picture>
14
14
  <source media="(prefers-color-scheme: dark)" srcset="docs/media/replay-dark.gif">
15
15
  <img src="docs/media/replay-light.gif" width="1100"
16
- alt="The tarmac map, live and then replayed. It opens on the live fleet, whose nodes are grouped into a frame per working directory, each labelled with its project. As the scrubber is dragged the frames give way to the flat record: sessions drawn as dials, each arc a context window, background agents as strips of text, with the account's five-hour and seven-day gauges above them. Sessions appear and disappear, the arcs fill and reset, the five-hour window fills and rolls over, and a banner names the minute being replayed until Back to live is pressed and the frames come back.">
16
+ alt="The tarmac map, live and then replayed. It opens on the live fleet: nodes grouped into a frame per working directory, each frame labelled with its project, sessions drawn as dials whose arc is the context window, background agents docked under the cards as strips of text, and the account's five-hour and seven-day gauges in the header. As the scrubber under the fleet is dragged the frames give way to the flat record, every node in a cell of one size, sessions still dials and agents still strips, each with its project and what it has cost. Sessions appear as the day walks past and the grid gains a cell for each, the arcs fill, the five-hour gauge fills and rolls over to zero, and a banner names the minute being replayed until Back to live is pressed and the frames come back.">
17
17
  </picture>
18
18
 
19
19
  [Quickstart](#quickstart) · [The map](#the-map) · [The curves](#the-curves) · [Install](#install) ·
@@ -26,15 +26,15 @@ documented surfaces only, never an internal format.
26
26
  npx @adrrr/tarmac # one-shot fleet table
27
27
  npx @adrrr/tarmac --watch # the same table, redrawn every 5s until ^C
28
28
  npx @adrrr/tarmac serve # the same fleet in the browser
29
- npx @adrrr/tarmac serve --demo # an invented fleet with a week of history, to see it full
29
+ npx @adrrr/tarmac serve --demo # an invented fleet with a month of history, to see it full
30
30
  npx @adrrr/tarmac install # chain the status line: unlocks ctx, model, effort and cost
31
31
  npx @adrrr/tarmac uninstall # hand your status line back
32
32
  ```
33
33
 
34
34
  On a fresh machine the first three of those are one session and no history, which is not much
35
35
  of a dashboard. `serve --demo` is the same dashboard over eight invented sessions, a day of
36
- their record and a week of journal under that, so the map, the replay and every range of the
37
- curves have something in them. It reads no fleet and writes nothing — the week it shows is
36
+ their record and a month of journal under that, so the map, the replay and every range of the
37
+ curves have something in them. It reads no fleet and writes nothing — the month it shows is
38
38
  invented in memory, like the rest of it — and the page it serves is badged `demo data`
39
39
  throughout.
40
40
 
@@ -84,7 +84,7 @@ listening rules are [there too](docs/MANUAL.md#what-serve-listens-on).
84
84
  <picture>
85
85
  <source media="(prefers-color-scheme: dark)" srcset="docs/media/map-dark.png">
86
86
  <img src="docs/media/map-light.png" width="1100"
87
- alt="The tarmac map, live. Four framed groups, one per working directory and labelled with its project: beacon, holding a session halted on a permission prompt; harbor, holding a session busy with its context arc at 90% and a background agent docked under the cards of that frame as a strip named after its prompt; quay, reporting a status tarmac does not know; and atlas, idle at 36%. A frame says only that its nodes were read in the same directory. Nothing inside one claims that any node dispatched another. The account's five-hour and seven-day gauges sit above them, and one warning above the fleet names the unrecognised status rather than filing it as idle.">
87
+ alt="The tarmac map, live. Five framed groups, one per working directory and labelled with its project: auth-service, holding a session halted on a permission prompt at 61%; api-gateway, holding a session busy with its context arc at 93% and a background agent docked under the cards of that frame as a strip named after its prompt; data-pipeline, busy at 69% with an agent of its own; docs-site, holding one session reporting a status tarmac does not know and one idle at 39%; and storefront, idle at 23%. A frame says only that its nodes were read in the same directory. Nothing inside one claims that any node dispatched another. The account's five-hour and seven-day gauges sit in the header, one warning above the fleet names the unrecognised status rather than filing it as idle, and the scrubber for the replay sits under the fleet.">
88
88
  </picture>
89
89
 
90
90
  The tabs in the header swap the table for the same fleet drawn as nodes: one per session, the
@@ -103,10 +103,13 @@ session gets its own frame rather than somebody else's.
103
103
  Above the fleet are the account's five-hour and seven-day
104
104
  [gauges](docs/MANUAL.md#the-accounts-two-windows). Under it is a scrubber over the day this
105
105
  serve has seen. Drag it and the nodes render the fleet as it was at that minute. Press play
106
- and the day walks past. Replayed, the nodes are drawn flat and unframed. The record keeps a
107
- project name and never the directory it was read in, and a basename is not a directory. Two
108
- checkouts of `atlas` answer to the same word, so a frame drawn on it would claim a shared
109
- directory nobody can check.
106
+ and the day walks past. Replayed, the nodes are drawn flat and unframed, every one of them in a
107
+ cell of one size: a session that appears between two minutes of a scrub moves nothing but the
108
+ nodes after it in the sample's own order, and the banner naming the minute stands in a place the
109
+ page keeps for it. A row the grid gains still pushes what is under it, and on a phone the banner
110
+ wraps where the live line does not. The record keeps a project name and never the directory it
111
+ was read in, and a basename is not a directory. Two checkouts of `atlas` answer to the same
112
+ word, so a frame drawn on it would claim a shared directory nobody can check.
110
113
 
111
114
  The map follows the rules the table follows. A reading past the freshness threshold is drawn
112
115
  thin, amber and dated. A percentage nobody measured is an empty dotted dial, never a ring at
@@ -126,7 +129,7 @@ the journal, if you keep one. `<canvas>` and the page's own script, no library.
126
129
  <picture>
127
130
  <source media="(prefers-color-scheme: dark)" srcset="docs/media/curves-dark.png">
128
131
  <img src="docs/media/curves-light.png" width="1100"
129
- alt="The curves page of the demo fleet, on the 7d range out of the invented journal. Context is a row of small multiples, one line per session, each a sawtooth that climbs through the day and breaks where the session was recycled, the two still climbing labelled with an arrow. Cost is one stacked bar per day in a fixed project order, the current day shorter than the six complete ones. Quota is the seven-day line climbing over the five-hour window's highs. A banner above names one unrecognised status, and the header is badged demo data.">
132
+ alt="The curves page of the demo fleet, on the 7d range out of the invented journal. Context is a row of small multiples, one band per project: four sawtooths that climb through the day and break where the session was recycled, and storefront, a short stroke a day near the baseline. None carries the arrow that marks a climbing line, since the panel counts nothing climbing over this range. Cost is one stacked bar per day in a fixed project order, seven days between $128 and $221. Quota is the seven-day line climbing over the five-hour window's highs. A banner above names one unrecognised status, and the header is badged demo data.">
130
133
  </picture>
131
134
 
132
135
  A context line breaks where its session did, so a recycle at three in the morning reads as a
@@ -234,11 +237,12 @@ and what would have worked. Spellings, edge cases and the two health fields
234
237
  ```bash
235
238
  npm test # typecheck (src + test + scripts), then run the suite
236
239
  npm run build # flat JavaScript into dist/
237
- node scripts/demo-fleet.ts # the invented fleet the captures above are taken of
240
+ node scripts/demo-fleet.ts # a second invented fleet, served on a port
238
241
  ```
239
242
 
240
- Every capture on this page is taken of a fleet that does not exist. A screenshot of a real
241
- machine carries working directories, prompts and costs, and nothing real enters this repo.
243
+ Every capture on this page is taken of `serve --demo`, a fleet that does not exist. A
244
+ screenshot of a real machine carries working directories, prompts and costs, and nothing real
245
+ enters this repo.
242
246
  What CI covers, which Node version develops and which one ships:
243
247
  [the manual](docs/MANUAL.md#developing). Capturing the fixtures for a new Claude Code build:
244
248
  [the manual](docs/MANUAL.md#capturing-a-new-claude-code-version).
@@ -1,4 +1,4 @@
1
- // The week of journal `serve --demo` shows, invented in memory.
1
+ // The month of journal `serve --demo` shows, invented in memory.
2
2
  //
3
3
  // Why it ships. `/history` was the one page of the demo that showed the product switched off:
4
4
  // "History is off.", the 7d and 30d pills greyed out, nothing for the range charts or the
@@ -18,8 +18,11 @@
18
18
  // and it exists to be reported and never to be opened.
19
19
  //
20
20
  // One story, not two. Every record is `demoFleetAt` played through the same `record` a real
21
- // sampler calls and then through the store's own allowlist, so the last day of the journal and
22
- // the ring behind the scrubber are the same readings, minute for minute.
21
+ // sampler calls and then through the store's own allowlist, so every minute the journal and the
22
+ // ring share is the same reading in both — which is the last TWO calendar days, the ring being
23
+ // twenty-four hours long and midnights being where they are. The days before those are that same
24
+ // day again, scaled: see `demoDayFactor`, which is where a month of columns stops being a month
25
+ // of one column.
23
26
  import { demoFleetAt, DEMO_HOME, DEMO_MINUTES } from './demo.js';
24
27
  import { createHistory } from './history.js';
25
28
  import { readRange } from './history-range.js';
@@ -27,11 +30,48 @@ import { journalRecordOf } from './history-store.js';
27
30
  /**
28
31
  * How many local days of it there are, today included.
29
32
  *
30
- * Seven and not thirty. A month is four times the work for a page that says what it covers
31
- * anyway, and a `30d` that answers the week it has is a range showing what exists — which beats
32
- * one invented badly, and is what a real journal younger than its retention already does.
33
+ * Thirty, which is what the longest range asks for. A week answered both pills with the same
34
+ * seven columns, so `30d` and `7d` drew the identical picture on the one serve built to show
35
+ * them full a range showing what exists is honest of a real journal younger than its
36
+ * retention, and pointless of a past that is invented anyway. Four times the records, and the
37
+ * whole month is built in about a third of a second.
33
38
  */
34
- export const DEMO_JOURNAL_DAYS = 7;
39
+ export const DEMO_JOURNAL_DAYS = 30;
40
+ /** A local day, in milliseconds. Used to count days apart, never to walk from one to the next. */
41
+ const DAY = 86_400_000;
42
+ /**
43
+ * What a day of the invented past cost, as a multiple of the day the ring holds.
44
+ *
45
+ * A month of identical columns at $147 is a chart that says "made up" before the reader has read
46
+ * the axis. A fleet's week has a shape — weekends nobody works, an ordinary run of weekdays, and
47
+ * now and then a day something big shipped — and that shape is the thing the cost chart exists
48
+ * to show, so the invented one has it too.
49
+ *
50
+ * Deterministic and stateless: a function of which day it is and nothing else, so two reads of
51
+ * one demo are one answer and a screenshot can be taken twice. The multiplier off Knuth's
52
+ * constant is a spread, not randomness — it just has to be unmemorable and stable.
53
+ *
54
+ * Never asked about a day the ring reaches into — `demoJournalDay` answers 1 for those without
55
+ * coming here, and there are two of them, the ring being twenty-four hours long. The one thing
56
+ * this journal may not do is disagree with the record behind the scrubber about a minute they
57
+ * both hold. What is left for this to scale is the past nobody can compare against.
58
+ *
59
+ * The long day is a weekday, which is what keeps the shape readable: the peaks are then above
60
+ * every ordinary day and the weekends below every one of them, rather than a Saturday at $220
61
+ * saying the opposite of what the chart is showing. Seventeen apart is at most two of them in a
62
+ * month and never none — seventeen days is three weekdays along, so the pair cannot both land on
63
+ * a weekend.
64
+ */
65
+ export function demoDayFactor(back, weekday) {
66
+ if (back <= 0)
67
+ return 1;
68
+ const spread = ((back * 2_654_435_761) % 1000) / 1000;
69
+ if (weekday === 0 || weekday === 6)
70
+ return 0.55 + 0.12 * spread;
71
+ if (back % 17 === 6)
72
+ return 1.5;
73
+ return 0.85 + 0.38 * spread;
74
+ }
35
75
  /**
36
76
  * Where the journal would live if there were one. Named because a store names its directory,
37
77
  * never opened by anything here — and under the invented home, so a capture of a demo carries
@@ -50,8 +90,9 @@ const MINUTE = 60_000;
50
90
  * whatever this says. A period shorter than the day plays the fleet arriving and going home
51
91
  * more than once between two midnights, which charges a fraction of a day's work to each of
52
92
  * them and can leave an actor born late in the day out of the week entirely. The suite holds
53
- * the consequence rather than the number: every full day of the invented week costs the same
54
- * and carries all five projects, which is true of a whole day repeated and of nothing else.
93
+ * the consequence rather than the number: every full day of the invented past carries all five
94
+ * projects, and each of them costs its own day's scale of one whole day's work which is true
95
+ * of a whole day repeated and of nothing else.
55
96
  *
56
97
  * What is NOT held, because it cannot be seen: whether an older cycle replays the day's last
57
98
  * minute or stops one short of it. Both tile the past exactly and both leave every day the same
@@ -136,15 +177,34 @@ export function demoJournalDay(date, dayStart) {
136
177
  // call — what is wanted is the reduction, and a second copy of that reduction here would be
137
178
  // the one thing this module exists not to be.
138
179
  const ring = createHistory({ since: dayStart, cadence: MINUTE });
180
+ // What this whole day cost, against the day the ring holds. Decided once for the day rather
181
+ // than per reading: the reader charges a day what a session's highest reading in it exceeds its
182
+ // lowest, and a multiplier that moved inside the day would be charged that movement as spending.
183
+ //
184
+ // A day the RING reaches into is never scaled, and that is two calendar days rather than one:
185
+ // `dayStart` is twenty-four hours back, so the ring straddles a midnight on every clock but
186
+ // midnight itself. Scaled on the older of the two, 719 of the ring's 1440 minutes went into the
187
+ // journal at another price — the cost chart billing yesterday 14% above what the scrubber shows
188
+ // for the same minutes, which is the one disagreement this module exists to prevent. `end` is
189
+ // this day's last moment, so a day the ring reaches into is one that ends at or after its first.
190
+ const factor = end >= dayStart ? 1 : demoDayFactor(Math.round((midnightOf(dayOf(last)) - midnight) / DAY), d.getDay());
139
191
  let text = '';
140
192
  for (let k = from; k <= to; k++) {
141
193
  const t = dayStart + k * MINUTE;
142
194
  const cycleStart = cycleStartFor(t, dayStart);
143
195
  const minute = Math.round((t - cycleStart) / MINUTE);
144
196
  // How many cycles before the newest day this reading belongs to — what lets the seven-day
145
- // window climb across the whole invented week instead of replaying one day's ramp.
146
- const cyclesBack = Math.round((dayStart - cycleStart) / CYCLE_MS);
147
- text += `${JSON.stringify(journalRecordOf(ring.record(demoFleetAt(minute, cycleStart, t, undefined, cyclesBack))))}\n`;
197
+ // window climb across a whole invented week instead of replaying one day's ramp. Modulo the
198
+ // week, because that is how long the window IS: a ramp running the length of the journal put
199
+ // the account four times over its plan by the middle of the month and then flat on the floor
200
+ // for the three weeks before it, where a real one rolls over every seven days.
201
+ const cyclesBack = Math.round((dayStart - cycleStart) / CYCLE_MS) % 7;
202
+ const record = journalRecordOf(ring.record(demoFleetAt(minute, cycleStart, t, undefined, cyclesBack)));
203
+ if (factor !== 1)
204
+ for (const s of record.sessions)
205
+ if (s.costUsd !== null)
206
+ s.costUsd = Math.round(s.costUsd * factor * 100) / 100;
207
+ text += `${JSON.stringify(record)}\n`;
148
208
  }
149
209
  return text;
150
210
  }
package/dist/fleet.js CHANGED
@@ -14,7 +14,7 @@ import path from 'node:path';
14
14
  import { DEFAULT_STALE_AFTER_MS } from './config.js';
15
15
  import { measured, windowsApart } from './limits.js';
16
16
  import { guardVersions } from './schema.js';
17
- import { isWaiting } from './sessions.js';
17
+ import { anchoredOnKind, isBackgroundAgent, isWaiting } from './sessions.js';
18
18
  import { SID_NAME } from './wrapper.js';
19
19
  export function buildFleet({ sessions, snapshots, now, staleAfterMs = DEFAULT_STALE_AFTER_MS, discovery = null, }) {
20
20
  const rows = sessions.map((s) => {
@@ -43,7 +43,14 @@ export function buildFleet({ sessions, snapshots, now, staleAfterMs = DEFAULT_ST
43
43
  };
44
44
  });
45
45
  rows.sort((a, b) => rank(a) - rank(b) || (b.ctxPct ?? -1) - (a.ctxPct ?? -1));
46
- const covered = rows.filter((r) => r.ctxState !== 'absent').length;
46
+ // The population the coverage numbers are about. An agent has no TUI, so it never draws a
47
+ // frame and no statusline can ever file a snapshot for it: counted among the blind, one
48
+ // agent beside one chained terminal read as "chained on 1/2 sessions — run tarmac install",
49
+ // remediation that is already done and cannot work for the entry that raised it (#29). The
50
+ // map made the same call for the agent strip; this is the fleet-wide line catching up.
51
+ const anchored = anchoredOnKind(rows);
52
+ const chainable = rows.filter((r) => !isBackgroundAgent(r, anchored));
53
+ const covered = chainable.filter((r) => r.ctxState !== 'absent').length;
47
54
  // Blind AND unfilable, in that order — this number exists to say how many of the blind will
48
55
  // stay blind, and both renderers subtract it from them. A session can be unfilable and
49
56
  // covered at the same time: a snapshot written by a pre-upgrade wrapper under a non-UUID
@@ -51,8 +58,10 @@ export function buildFleet({ sessions, snapshots, now, staleAfterMs = DEFAULT_ST
51
58
  // than on the filename. Counting that one would push this past the blind count and make the
52
59
  // renderers explain away someone else's missing telemetry.
53
60
  // A null id is `noSessionId`'s business — a discovery failure, not a naming one.
54
- const unfilable = rows.filter((r) => r.ctxState === 'absent' && r.sessionId !== null && !SID_NAME.test(r.sessionId)).length;
55
- const drift = rows.filter((r) => r.ctxState === 'drift').length;
61
+ const unfilable = chainable.filter((r) => r.ctxState === 'absent' && r.sessionId !== null && !SID_NAME.test(r.sessionId)).length;
62
+ // Over the same population as `covered`, because `schemaBroken` below compares the two and a
63
+ // comparison between two different populations answers a question nobody asked.
64
+ const drift = chainable.filter((r) => r.ctxState === 'drift').length;
56
65
  // Having a snapshot and having a cost are different facts, and only the second one is
57
66
  // allowed to feed the total.
58
67
  const costs = rows.map((r) => r.costUsd).filter((c) => typeof c === 'number');
@@ -67,6 +76,7 @@ export function buildFleet({ sessions, snapshots, now, staleAfterMs = DEFAULT_ST
67
76
  rows,
68
77
  health: {
69
78
  sessions: rows.length,
79
+ chainable: chainable.length,
70
80
  covered,
71
81
  unfilable,
72
82
  drift,
@@ -221,6 +221,8 @@ export async function readRange({ dir, range, now, capped = false, readDay }) {
221
221
  }
222
222
  return {
223
223
  range,
224
+ from: windowStart,
225
+ to: windowEnd,
224
226
  hours: [...hours.entries()]
225
227
  .sort(([a], [b]) => a - b)
226
228
  .map(([t, acc]) => ({