agentfootprint-lens 0.47.1 → 0.48.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
@@ -1061,6 +1061,121 @@ on every epoch, no receipt was minted, and the tab says so with the library's
1061
1061
 
1062
1062
  ---
1063
1063
 
1064
+ ## Bookmarks, and scrubbing by declared tag
1065
+
1066
+ **The reader's marks, kept beside the recording — and the author's tags, as a
1067
+ ruler.** Since 0.48.0 the Why Lens's right rail has a third tab, **Bookmarks**,
1068
+ and the grouped ruler carries a **Tags** strip.
1069
+
1070
+ ### Why
1071
+
1072
+ footprintjs's design page for declared tags names three marks that end in the
1073
+ same operation ("keep these stops, fold the rest into them") and differ in who
1074
+ puts the mark and when:
1075
+
1076
+ | Mark | Who, when | Lives | In the lens |
1077
+ |---|---|---|---|
1078
+ | **Declared tag** | the author, at build time | the chart's structure → stamped into the commit bundle (`bundle.tags`) | the **Tags** strip: legend and picker |
1079
+ | **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 |
1080
+ | **Bookmark** | the reader, at read time, by choice | on the cursor (`mark` / `marks` / `jumpToMark`); persisted beside a recording, never in it | the **Bookmarks** tab |
1081
+
1082
+ Only the first two had a place in the lens. footprintjs holds a bookmark on the
1083
+ cursor and, by its own law, nowhere else — the log and the snapshot are the
1084
+ run's record, not the reader's notes — so a bookmark vanished when the page
1085
+ closed. And a run's own tags (agentfootprint 9.90 declares every milestone as
1086
+ one) were in the recording with nothing to read them.
1087
+
1088
+ ### Bookmarks
1089
+
1090
+ Stand on a stop and press **Bookmark this stop**. The library names the mark
1091
+ (its own `mark()`, seated on the lens's one cursor); the lens writes it to a
1092
+ **sidecar** — a small document keyed to the run, in a store you choose. Click a
1093
+ bookmark to jump: the port's `jumpToMark`, through the same funnel every other
1094
+ mover uses, so the chart, the timeline and the Served tab all follow. Add a
1095
+ note inline; remove with one click.
1096
+
1097
+ ```tsx
1098
+ import { Lens } from 'agentfootprint-lens';
1099
+ import { memoryBookmarkStore } from 'agentfootprint-lens/core';
1100
+
1101
+ <Lens recorder={recorder} runner={agent} granularity="group" /> // localStorage, by default
1102
+ <Lens recorder={recorder} runner={agent} bookmarkStore={memoryBookmarkStore()} /> // this session only
1103
+ ```
1104
+
1105
+ The laws the tab keeps:
1106
+
1107
+ - **One cursor.** The tab holds a list, never a position. A jump moves the lens
1108
+ cursor (`onStepChange` fires); a miss never moves.
1109
+ - **Never in the record.** Nothing is written into the snapshot or the log.
1110
+ - **Omit, never deny.** A bookmark whose stop this recording does not hold —
1111
+ a truncated recording, a sidecar filed under another run — is shown greyed as
1112
+ *not in this recording*, never dropped.
1113
+ - **Persistence is a label.** A browser that refuses storage (private mode, a
1114
+ quota, a server render) makes the tab say *bookmarks not saved*; a snapshot
1115
+ with no `runId` says *no run id*. Nothing throws.
1116
+ - **No claim sentences.** Every string the tab prints is a label
1117
+ (`BOOKMARK_LABELS`) or computed data; `test/served/no-own-claims.test.ts`
1118
+ walks the file.
1119
+
1120
+ The root bookends (*Run · start* / *Run · end*) are not bookmarkable — Home and
1121
+ End reach them on every axis, and they share one synthetic address.
1122
+
1123
+ Headless, the sidecar is four functions in `/core`: `bookmarkKey(snapshot)`,
1124
+ `toSidecar(key, bookmarks)`, `fromSidecar(json, snapshot)` (reports
1125
+ `orphaned`), `bookmarksToMarks(bookmarks, positions)` to seed footprintjs's
1126
+ `timeTravel(snapshot, { marks })`; stores are `localStorageBookmarkStore()`,
1127
+ `memoryBookmarkStore()`, `noBookmarkStore()`, or any `BookmarkStore` of your
1128
+ own. See `src/core/bookmarks/README.md`.
1129
+
1130
+ ### The Tags strip
1131
+
1132
+ On the grouped ruler, above the chart: one chip per declared tag, labelled with
1133
+ agentfootprint's milestone label when the tag is a milestone (*LLM turn*,
1134
+ *Route*) and its raw name otherwise (*audit*), with the number of bundles this
1135
+ run stamped it on. The list comes from the recording's **structure** — the
1136
+ chart advertises the tags it *can* produce, so the legend is drawn before a
1137
+ stop is reached — and the counts from the **log**. A recording that carried no
1138
+ structure lists only *tags this run hit* and says *no chart in this recording*.
1139
+ A declared tag the run never hit is shown and disabled.
1140
+
1141
+ **Pick a chip and the ruler is rebuilt** through footprintjs 9.21's
1142
+ `tagStops(names)`: the stops whose bundle carries any picked tag, the untagged
1143
+ stages folded into the tagged stop before them, the run's bookends kept. It is
1144
+ the same one cursor on a different list — the cursor **keeps its commit**, not
1145
+ its step number: it is re-seated at the stop that holds its commit (nearest
1146
+ preceding when the new axis has none there — `stepForCommitIdx`, the same rule
1147
+ the `granularity` doc gives a host), and the host hears it as an ordinary
1148
+ `onStepChange`. `stateAt` at a picked stop equals the default axis's fold at
1149
+ the same commit (`src/core/tags/tags.test.ts` pins the equivalence). *Clear*
1150
+ restores the lens's own grouping, by the same rule. The strip appears on
1151
+ `granularity="group"` at the root level only: the per-step reading's ruler is
1152
+ every commit, byte for byte, and a drilled level scrubs its own stops.
1153
+
1154
+ A pick scrubs the run's OWN log (`tagStops` reads no other), so a chip is
1155
+ pickable only when the tag was hit on the root log; a tag hit only inside
1156
+ mounted subflows (the grouped turn's *LLM turn*, the context slots) is shown
1157
+ disabled with *in subflows* — real, said, and not a stop on this axis. Counts
1158
+ walk each mount's log once (`subflowResults` is dual-keyed; only the `#n`
1159
+ entries are read).
1160
+
1161
+ ```ts
1162
+ import { tagLegend, tagAxisPositions, scrubAxisFor } from 'agentfootprint-lens/core';
1163
+
1164
+ const legend = tagLegend(runner.getSpec().buildTimeStructure, runner.getLastSnapshot());
1165
+ legend.source; // 'structure' | 'log'
1166
+ legend.entries.map((e) => [e.label, e.hits]); // [['Iteration', 2], ['LLM turn', 2], ['audit', 0], …]
1167
+
1168
+ const axis = tagAxisPositions(snapshot, ['milestone:llm-turn'], scrubAxisFor(recorder, 'group'));
1169
+ axis?.map((p) => p.label); // ['Run · start', 'LLM turn 1', 'LLM turn 2', 'Run · end']
1170
+ ```
1171
+
1172
+ **Older peers.** `tagStops` (footprintjs 9.21) and `milestoneFromTags`
1173
+ (agentfootprint 9.90) are read off their module namespaces at call time: on a
1174
+ peer without them the legend prints raw names, the strip says *tag axis
1175
+ unavailable*, and nothing else changes. The peer ranges are untouched.
1176
+
1177
+ ---
1178
+
1064
1179
  ## Rendering your own detail pane
1065
1180
 
1066
1181
  `slots.detail` replaces the CONTENT of the shipped right column. The column
@@ -1290,6 +1405,27 @@ field, the library's gap sentences verbatim, the fold's hidden skill ids, and a
1290
1405
  since-previous diff. Headless: `servedRowAt` · `verify` · `sincePrevious` ·
1291
1406
  `foldFactsAt` in `/core`. See "The Served tab" above.
1292
1407
 
1408
+ ### `<BookmarksTab>` — the reader's marks, riding the one cursor
1409
+
1410
+ `<BookmarksTab bookmarks orphaned persistence positions step port moveTo onAdd
1411
+ onRemove onNote>`. The Why Lens mounts it as the right rail's third tab
1412
+ (`<Lens bookmarkStore?>` picks the store; default `localStorageBookmarkStore()`);
1413
+ exported for consumer-built shells, with `useBookmarkSidecar({ snapshot, store })`
1414
+ as the list and `openLensCursor(positions, { marks })` as the port. A jump is
1415
+ the port's `toMark` through `moveTo`; orphans are greyed and labelled; the
1416
+ store's availability is a label. Headless: `bookmarkKey` · `toSidecar` ·
1417
+ `fromSidecar` · `bookmarksToMarks` and the three stores in `/core`. See
1418
+ "Bookmarks, and scrubbing by declared tag" above.
1419
+
1420
+ ### `<TagPicker>` — the declared-tag legend and picker
1421
+
1422
+ `<TagPicker legend picked onPick available>`. The engineer view mounts it above
1423
+ the chart on `granularity="group"` at the root level; `legend` is
1424
+ `tagLegend(structure, snapshot)` (`source: 'structure' | 'log'`), a pick is a
1425
+ list of tag names the lens hands to `tagAxisPositions(snapshot, picked, base)`
1426
+ (footprintjs 9.21's `tagStops`). Picking never moves the cursor; it changes the
1427
+ list the one cursor is a step into.
1428
+
1293
1429
  ### `<BugReportButton>` — report a bug with the run attached, consent first
1294
1430
 
1295
1431
  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,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  formatBytes,
3
3
  structureGraphFromRunner
4
- } from "./chunk-4PENVNUY.js";
4
+ } from "./chunk-FZHRQNRS.js";
5
5
 
6
6
  // src/core/buildStepGraphFromSnapshot.ts
7
7
  function buildStepGraphFromSnapshot(snapshot) {
@@ -1189,4 +1189,4 @@ export {
1189
1189
  decisionSentence,
1190
1190
  isConsentDecision
1191
1191
  };
1192
- //# sourceMappingURL=chunk-XFNFYUPC.js.map
1192
+ //# sourceMappingURL=chunk-DQHZE7V7.js.map