agentfootprint-lens 0.47.1 → 0.49.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
@@ -1059,6 +1059,165 @@ on every epoch, no receipt was minted, and the tab says so with the library's
1059
1059
  `no-receipt-on-chart` gap and `cause: 'no-receipt-committed'` — every row
1060
1060
  **Reconstructed**, never a fabricated **Verified**.
1061
1061
 
1062
+ ### The Served graph (0.49.0)
1063
+
1064
+ A list is right for checking one row and wrong for seeing that the system string
1065
+ was assembled from a base plus one skill body plus two injections while three
1066
+ tools were withheld. That is one picture, and since 0.49.0 the Served tab has a
1067
+ **List ⇄ Graph** toggle (the list stays the default) that draws it: **HELD** —
1068
+ what the record holds at this stop, from the fold — then **SERVED** — what
1069
+ crossed into the call, one edge per system piece, message, request-only line and
1070
+ tool, each carrying its badge and, against the previous epoch, whether it
1071
+ *entered*, *left* or is *unchanged* — then **WITHHELD**: held and not sent. The
1072
+ withheld band is the reason to build it. The model never learns what it was
1073
+ denied; the operator should — the withheld tool list, the attention drops, the
1074
+ skills a role hid (read from the fold, since a receipt carries no authority
1075
+ names), a redacted fold, and every gap the view declares, each with the
1076
+ library's own sentence. Same cursor, same row, same badges: a Damaged row draws
1077
+ Damaged here too, and a field a gap covers draws the gap rather than an empty
1078
+ node.
1079
+
1080
+ | graph | what it draws | the question it answers |
1081
+ |---|---|---|
1082
+ | **skill graph** (`<SkillGraph>`) | the skills, their steps and their edges | where a run MAY go, and where it went |
1083
+ | **data graph** (*Where from*) | a value's writers, back through the commit log | where THIS value came from |
1084
+ | **Served graph** (Served tab · Graph) | one call: held · served · withheld | what this ONE CALL was made of, and what we held back |
1085
+
1086
+ ```ts
1087
+ import { foldFactsAt, servedGraphAt, servedRowAt, verify } from 'agentfootprint-lens/core';
1088
+
1089
+ const cursor = { runtimeStageId: 'call-llm#18', commitIdx: 15 };
1090
+ const row = servedRowAt(snapshot, cursor)!;
1091
+ const graph = servedGraphAt({
1092
+ row,
1093
+ fold: foldFactsAt(snapshot, cursor),
1094
+ checks: verify(row.view, row.receipt, row.receipt?.basis.runId ?? '', row.receiptCause),
1095
+ });
1096
+ graph.served.map((s) => `${s.slot} ${s.rebuilt}`); // ['system-prompt 3', …]
1097
+ graph.edges.filter((e) => e.state === 'entered').map((e) => e.origin); // ['instructions']
1098
+ graph.withheld.filter((w) => w.from === 'fold').map((w) => w.name); // ['payroll']
1099
+ ```
1100
+
1101
+ `<ServedGraph graph>` is exported for shells that build the input themselves.
1102
+ **Deliberately absent:** an edge from a served piece back to the STAGE that
1103
+ wrote it. It needs a commit-log walk keyed by piece text or slot — the part most
1104
+ likely to rot — and the question above is answered without it.
1105
+
1106
+ ---
1107
+
1108
+ ## Bookmarks, and scrubbing by declared tag
1109
+
1110
+ **The reader's marks, kept beside the recording — and the author's tags, as a
1111
+ ruler.** Since 0.48.0 the Why Lens's right rail has a third tab, **Bookmarks**,
1112
+ and the grouped ruler carries a **Tags** strip.
1113
+
1114
+ ### Why
1115
+
1116
+ footprintjs's design page for declared tags names three marks that end in the
1117
+ same operation ("keep these stops, fold the rest into them") and differ in who
1118
+ puts the mark and when:
1119
+
1120
+ | Mark | Who, when | Lives | In the lens |
1121
+ |---|---|---|---|
1122
+ | **Declared tag** | the author, at build time | the chart's structure → stamped into the commit bundle (`bundle.tags`) | the **Tags** strip: legend and picker |
1123
+ | **Derived tag** | the reader, at read time, from the fold | computed by a strategy; never stored | the lens's own grouping (milestones, banded by iteration) — the default axis |
1124
+ | **Bookmark** | the reader, at read time, by choice | on the cursor (`mark` / `marks` / `jumpToMark`); persisted beside a recording, never in it | the **Bookmarks** tab |
1125
+
1126
+ Only the first two had a place in the lens. footprintjs holds a bookmark on the
1127
+ cursor and, by its own law, nowhere else — the log and the snapshot are the
1128
+ run's record, not the reader's notes — so a bookmark vanished when the page
1129
+ closed. And a run's own tags (agentfootprint 9.90 declares every milestone as
1130
+ one) were in the recording with nothing to read them.
1131
+
1132
+ ### Bookmarks
1133
+
1134
+ Stand on a stop and press **Bookmark this stop**. The library names the mark
1135
+ (its own `mark()`, seated on the lens's one cursor); the lens writes it to a
1136
+ **sidecar** — a small document keyed to the run, in a store you choose. Click a
1137
+ bookmark to jump: the port's `jumpToMark`, through the same funnel every other
1138
+ mover uses, so the chart, the timeline and the Served tab all follow. Add a
1139
+ note inline; remove with one click.
1140
+
1141
+ ```tsx
1142
+ import { Lens } from 'agentfootprint-lens';
1143
+ import { memoryBookmarkStore } from 'agentfootprint-lens/core';
1144
+
1145
+ <Lens recorder={recorder} runner={agent} granularity="group" /> // localStorage, by default
1146
+ <Lens recorder={recorder} runner={agent} bookmarkStore={memoryBookmarkStore()} /> // this session only
1147
+ ```
1148
+
1149
+ The laws the tab keeps:
1150
+
1151
+ - **One cursor.** The tab holds a list, never a position. A jump moves the lens
1152
+ cursor (`onStepChange` fires); a miss never moves.
1153
+ - **Never in the record.** Nothing is written into the snapshot or the log.
1154
+ - **Omit, never deny.** A bookmark whose stop this recording does not hold —
1155
+ a truncated recording, a sidecar filed under another run — is shown greyed as
1156
+ *not in this recording*, never dropped.
1157
+ - **Persistence is a label.** A browser that refuses storage (private mode, a
1158
+ quota, a server render) makes the tab say *bookmarks not saved*; a snapshot
1159
+ with no `runId` says *no run id*. Nothing throws.
1160
+ - **No claim sentences.** Every string the tab prints is a label
1161
+ (`BOOKMARK_LABELS`) or computed data; `test/served/no-own-claims.test.ts`
1162
+ walks the file.
1163
+
1164
+ The root bookends (*Run · start* / *Run · end*) are not bookmarkable — Home and
1165
+ End reach them on every axis, and they share one synthetic address.
1166
+
1167
+ Headless, the sidecar is four functions in `/core`: `bookmarkKey(snapshot)`,
1168
+ `toSidecar(key, bookmarks)`, `fromSidecar(json, snapshot)` (reports
1169
+ `orphaned`), `bookmarksToMarks(bookmarks, positions)` to seed footprintjs's
1170
+ `timeTravel(snapshot, { marks })`; stores are `localStorageBookmarkStore()`,
1171
+ `memoryBookmarkStore()`, `noBookmarkStore()`, or any `BookmarkStore` of your
1172
+ own. See `src/core/bookmarks/README.md`.
1173
+
1174
+ ### The Tags strip
1175
+
1176
+ On the grouped ruler, above the chart: one chip per declared tag, labelled with
1177
+ agentfootprint's milestone label when the tag is a milestone (*LLM turn*,
1178
+ *Route*) and its raw name otherwise (*audit*), with the number of bundles this
1179
+ run stamped it on. The list comes from the recording's **structure** — the
1180
+ chart advertises the tags it *can* produce, so the legend is drawn before a
1181
+ stop is reached — and the counts from the **log**. A recording that carried no
1182
+ structure lists only *tags this run hit* and says *no chart in this recording*.
1183
+ A declared tag the run never hit is shown and disabled.
1184
+
1185
+ **Pick a chip and the ruler is rebuilt** through footprintjs 9.21's
1186
+ `tagStops(names)`: the stops whose bundle carries any picked tag, the untagged
1187
+ stages folded into the tagged stop before them, the run's bookends kept. It is
1188
+ the same one cursor on a different list — the cursor **keeps its commit**, not
1189
+ its step number: it is re-seated at the stop that holds its commit (nearest
1190
+ preceding when the new axis has none there — `stepForCommitIdx`, the same rule
1191
+ the `granularity` doc gives a host), and the host hears it as an ordinary
1192
+ `onStepChange`. `stateAt` at a picked stop equals the default axis's fold at
1193
+ the same commit (`src/core/tags/tags.test.ts` pins the equivalence). *Clear*
1194
+ restores the lens's own grouping, by the same rule. The strip appears on
1195
+ `granularity="group"` at the root level only: the per-step reading's ruler is
1196
+ every commit, byte for byte, and a drilled level scrubs its own stops.
1197
+
1198
+ A pick scrubs the run's OWN log (`tagStops` reads no other), so a chip is
1199
+ pickable only when the tag was hit on the root log; a tag hit only inside
1200
+ mounted subflows (the grouped turn's *LLM turn*, the context slots) is shown
1201
+ disabled with *in subflows* — real, said, and not a stop on this axis. Counts
1202
+ walk each mount's log once (`subflowResults` is dual-keyed; only the `#n`
1203
+ entries are read).
1204
+
1205
+ ```ts
1206
+ import { tagLegend, tagAxisPositions, scrubAxisFor } from 'agentfootprint-lens/core';
1207
+
1208
+ const legend = tagLegend(runner.getSpec().buildTimeStructure, runner.getLastSnapshot());
1209
+ legend.source; // 'structure' | 'log'
1210
+ legend.entries.map((e) => [e.label, e.hits]); // [['Iteration', 2], ['LLM turn', 2], ['audit', 0], …]
1211
+
1212
+ const axis = tagAxisPositions(snapshot, ['milestone:llm-turn'], scrubAxisFor(recorder, 'group'));
1213
+ axis?.map((p) => p.label); // ['Run · start', 'LLM turn 1', 'LLM turn 2', 'Run · end']
1214
+ ```
1215
+
1216
+ **Older peers.** `tagStops` (footprintjs 9.21) and `milestoneFromTags`
1217
+ (agentfootprint 9.90) are read off their module namespaces at call time: on a
1218
+ peer without them the legend prints raw names, the strip says *tag axis
1219
+ unavailable*, and nothing else changes. The peer ranges are untouched.
1220
+
1062
1221
  ---
1063
1222
 
1064
1223
  ## Rendering your own detail pane
@@ -1290,6 +1449,27 @@ field, the library's gap sentences verbatim, the fold's hidden skill ids, and a
1290
1449
  since-previous diff. Headless: `servedRowAt` · `verify` · `sincePrevious` ·
1291
1450
  `foldFactsAt` in `/core`. See "The Served tab" above.
1292
1451
 
1452
+ ### `<BookmarksTab>` — the reader's marks, riding the one cursor
1453
+
1454
+ `<BookmarksTab bookmarks orphaned persistence positions step port moveTo onAdd
1455
+ onRemove onNote>`. The Why Lens mounts it as the right rail's third tab
1456
+ (`<Lens bookmarkStore?>` picks the store; default `localStorageBookmarkStore()`);
1457
+ exported for consumer-built shells, with `useBookmarkSidecar({ snapshot, store })`
1458
+ as the list and `openLensCursor(positions, { marks })` as the port. A jump is
1459
+ the port's `toMark` through `moveTo`; orphans are greyed and labelled; the
1460
+ store's availability is a label. Headless: `bookmarkKey` · `toSidecar` ·
1461
+ `fromSidecar` · `bookmarksToMarks` and the three stores in `/core`. See
1462
+ "Bookmarks, and scrubbing by declared tag" above.
1463
+
1464
+ ### `<TagPicker>` — the declared-tag legend and picker
1465
+
1466
+ `<TagPicker legend picked onPick available>`. The engineer view mounts it above
1467
+ the chart on `granularity="group"` at the root level; `legend` is
1468
+ `tagLegend(structure, snapshot)` (`source: 'structure' | 'log'`), a pick is a
1469
+ list of tag names the lens hands to `tagAxisPositions(snapshot, picked, base)`
1470
+ (footprintjs 9.21's `tagStops`). Picking never moves the cursor; it changes the
1471
+ list the one cursor is a step into.
1472
+
1293
1473
  ### `<BugReportButton>` — report a bug with the run attached, consent first
1294
1474
 
1295
1475
  A small button for a debug UI. The dialog it opens shows every selectable unit
@@ -4,7 +4,7 @@ import * as agentfootprint from 'agentfootprint';
4
4
  import { CommentaryTemplates } from 'agentfootprint';
5
5
  import React__default, { Ref } from 'react';
6
6
  import { d as NavigationResult, C as CursorPosition, L as LensRecorder } from './resolveNavigation-Ckr2nXHY.cjs';
7
- import { C as ChartGroupHighlight, a as LensCursorPort, H as Humanizer } from './lensStops-DfNxsaH0.cjs';
7
+ import { C as ChartGroupHighlight, c as LensCursorPort, H as Humanizer, b as BookmarkStore } from './tagAxis-B598GxMJ.cjs';
8
8
  import { NodeTypes } from '@xyflow/react';
9
9
  import { TraceGraph, TraceFlowLayout, RuntimeOverlay } from 'footprint-explainable-ui/flowchart';
10
10
 
@@ -523,8 +523,22 @@ interface LensProps {
523
523
  * granularities carries it by `commitIdx`: build the target axis with
524
524
  * `scrubAxisFor(recorder, granularity)` and resolve it with
525
525
  * `stepForCommitIdx(positions, at.commitIdx)`.
526
+ *
527
+ * (0.48.0) The TAGS strip — the declared-tag legend and the picker that
528
+ * rebuilds the ruler through `tagStops` — appears on `'group'` at the root
529
+ * level only; `'step'` scrubs every commit and carries no strip. A pick
530
+ * re-seats the cursor by the same `stepForCommitIdx` rule.
526
531
  */
527
532
  readonly granularity?: 'step' | 'group';
533
+ /**
534
+ * Where the reader's BOOKMARKS persist (0.48.0). A bookmark is the reader's
535
+ * mark — never written into the recording — kept in a sidecar keyed to the
536
+ * run and seeded onto the cursor when the recording is opened. Default:
537
+ * `localStorageBookmarkStore()`; a browser that refuses storage makes the
538
+ * Bookmarks tab say "bookmarks not saved" rather than throw. Pass
539
+ * `memoryBookmarkStore()` for a session-only reading, or your own store.
540
+ */
541
+ readonly bookmarkStore?: BookmarkStore;
528
542
  /**
529
543
  * Controlled cursor (omit for uncontrolled). Omit it and the lens is
530
544
  * self-driving exactly as it has always been: it holds the position itself,
@@ -4,7 +4,7 @@ import * as agentfootprint from 'agentfootprint';
4
4
  import { CommentaryTemplates } from 'agentfootprint';
5
5
  import React__default, { Ref } from 'react';
6
6
  import { d as NavigationResult, C as CursorPosition, L as LensRecorder } from './resolveNavigation-Ckr2nXHY.js';
7
- import { C as ChartGroupHighlight, a as LensCursorPort, H as Humanizer } from './lensStops-Di41Rg3f.js';
7
+ import { C as ChartGroupHighlight, c as LensCursorPort, H as Humanizer, b as BookmarkStore } from './tagAxis-C__DMHiD.js';
8
8
  import { NodeTypes } from '@xyflow/react';
9
9
  import { TraceGraph, TraceFlowLayout, RuntimeOverlay } from 'footprint-explainable-ui/flowchart';
10
10
 
@@ -523,8 +523,22 @@ interface LensProps {
523
523
  * granularities carries it by `commitIdx`: build the target axis with
524
524
  * `scrubAxisFor(recorder, granularity)` and resolve it with
525
525
  * `stepForCommitIdx(positions, at.commitIdx)`.
526
+ *
527
+ * (0.48.0) The TAGS strip — the declared-tag legend and the picker that
528
+ * rebuilds the ruler through `tagStops` — appears on `'group'` at the root
529
+ * level only; `'step'` scrubs every commit and carries no strip. A pick
530
+ * re-seats the cursor by the same `stepForCommitIdx` rule.
526
531
  */
527
532
  readonly granularity?: 'step' | 'group';
533
+ /**
534
+ * Where the reader's BOOKMARKS persist (0.48.0). A bookmark is the reader's
535
+ * mark — never written into the recording — kept in a sidecar keyed to the
536
+ * run and seeded onto the cursor when the recording is opened. Default:
537
+ * `localStorageBookmarkStore()`; a browser that refuses storage makes the
538
+ * Bookmarks tab say "bookmarks not saved" rather than throw. Pass
539
+ * `memoryBookmarkStore()` for a session-only reading, or your own store.
540
+ */
541
+ readonly bookmarkStore?: BookmarkStore;
528
542
  /**
529
543
  * Controlled cursor (omit for uncontrolled). Omit it and the lens is
530
544
  * self-driving exactly as it has always been: it holds the position itself,