agentfootprint-lens 0.48.0 → 0.50.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
@@ -940,7 +940,9 @@ not answer, since it takes an exact stop.
940
940
  ## The Served tab
941
941
 
942
942
  **At every LLM call, exactly what the model was served — provable from the log.**
943
- The Why Lens's right rail has a second tab, **Served**, mounted for every run.
943
+ The Why Lens's right rail has a second tab, **Served**, mounted for every run —
944
+ including for a host that supplies `slots.detail`, whose own pane is the tab
945
+ beside it (0.50.0).
944
946
  Stand on an LLM turn and it shows the request that call went out with: the system
945
947
  prompt (piece by piece), the messages as sent, the tools as sent, the dials, the
946
948
  cache breakpoints — and beside every field, what the record can PROVE about it.
@@ -1059,12 +1061,57 @@ on every epoch, no receipt was minted, and the tab says so with the library's
1059
1061
  `no-receipt-on-chart` gap and `cause: 'no-receipt-committed'` — every row
1060
1062
  **Reconstructed**, never a fabricated **Verified**.
1061
1063
 
1064
+ ### The Served graph (0.49.0)
1065
+
1066
+ A list is right for checking one row and wrong for seeing that the system string
1067
+ was assembled from a base plus one skill body plus two injections while three
1068
+ tools were withheld. That is one picture, and since 0.49.0 the Served tab has a
1069
+ **List ⇄ Graph** toggle (the list stays the default) that draws it: **HELD** —
1070
+ what the record holds at this stop, from the fold — then **SERVED** — what
1071
+ crossed into the call, one edge per system piece, message, request-only line and
1072
+ tool, each carrying its badge and, against the previous epoch, whether it
1073
+ *entered*, *left* or is *unchanged* — then **WITHHELD**: held and not sent. The
1074
+ withheld band is the reason to build it. The model never learns what it was
1075
+ denied; the operator should — the withheld tool list, the attention drops, the
1076
+ skills a role hid (read from the fold, since a receipt carries no authority
1077
+ names), a redacted fold, and every gap the view declares, each with the
1078
+ library's own sentence. Same cursor, same row, same badges: a Damaged row draws
1079
+ Damaged here too, and a field a gap covers draws the gap rather than an empty
1080
+ node.
1081
+
1082
+ | graph | what it draws | the question it answers |
1083
+ |---|---|---|
1084
+ | **skill graph** (`<SkillGraph>`) | the skills, their steps and their edges | where a run MAY go, and where it went |
1085
+ | **data graph** (*Where from*) | a value's writers, back through the commit log | where THIS value came from |
1086
+ | **Served graph** (Served tab · Graph) | one call: held · served · withheld | what this ONE CALL was made of, and what we held back |
1087
+
1088
+ ```ts
1089
+ import { foldFactsAt, servedGraphAt, servedRowAt, verify } from 'agentfootprint-lens/core';
1090
+
1091
+ const cursor = { runtimeStageId: 'call-llm#18', commitIdx: 15 };
1092
+ const row = servedRowAt(snapshot, cursor)!;
1093
+ const graph = servedGraphAt({
1094
+ row,
1095
+ fold: foldFactsAt(snapshot, cursor),
1096
+ checks: verify(row.view, row.receipt, row.receipt?.basis.runId ?? '', row.receiptCause),
1097
+ });
1098
+ graph.served.map((s) => `${s.slot} ${s.rebuilt}`); // ['system-prompt 3', …]
1099
+ graph.edges.filter((e) => e.state === 'entered').map((e) => e.origin); // ['instructions']
1100
+ graph.withheld.filter((w) => w.from === 'fold').map((w) => w.name); // ['payroll']
1101
+ ```
1102
+
1103
+ `<ServedGraph graph>` is exported for shells that build the input themselves.
1104
+ **Deliberately absent:** an edge from a served piece back to the STAGE that
1105
+ wrote it. It needs a commit-log walk keyed by piece text or slot — the part most
1106
+ likely to rot — and the question above is answered without it.
1107
+
1062
1108
  ---
1063
1109
 
1064
1110
  ## Bookmarks, and scrubbing by declared tag
1065
1111
 
1066
1112
  **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**,
1113
+ ruler.** Since 0.48.0 the Why Lens's right rail has a third tab, **Bookmarks**
1114
+ (a fourth when a host fills `slots.detail`),
1068
1115
  and the grouped ruler carries a **Tags** strip.
1069
1116
 
1070
1117
  ### Why
@@ -1178,9 +1225,17 @@ unavailable*, and nothing else changes. The peer ranges are untouched.
1178
1225
 
1179
1226
  ## Rendering your own detail pane
1180
1227
 
1181
- `slots.detail` replaces the CONTENT of the shipped right column. The column
1182
- itself — its width, its border, its collapse pill, its cursor — is unchanged.
1183
- Omit `slots` and the built-in timeline renders exactly as before.
1228
+ **A slot fills a PANE, never the CHROME around it.** The right rail's tab strip,
1229
+ the one cursor and the collapse pill belong to the library, because chrome is how
1230
+ the library adds capability over time — a consumer that fills a slot must still
1231
+ receive every reading the rail gains later. That is why `slots.detail` is ONE TAB
1232
+ and not a takeover, and why `detailOnly` is an opt-out rather than the default.
1233
+
1234
+ `slots.detail` renders the right column's FIRST tab. The column itself — its
1235
+ width, its border, its collapse pill, its cursor — is unchanged, and so is the
1236
+ strip: your pane is selected by default, with *What happened*, *Served* and
1237
+ *Bookmarks* one click away beside it. Omit `slots` and the built-in timeline
1238
+ renders exactly as before.
1184
1239
 
1185
1240
  ```tsx
1186
1241
  const Detail: React.FC<LensDetailSlotProps> = ({ step, cursorRuntimeStageId, node, onNavigate }) => (
@@ -1191,7 +1246,8 @@ const Detail: React.FC<LensDetailSlotProps> = ({ step, cursorRuntimeStageId, nod
1191
1246
  </div>
1192
1247
  );
1193
1248
 
1194
- <Lens recorder={recorder} runner={runner} slots={{ detail: Detail }} />
1249
+ <Lens recorder={recorder} runner={runner} slots={{ detail: Detail, detailLabel: 'My bands' }} />
1250
+ // rail: [ My bands* | What happened | Served | Bookmarks ]
1195
1251
  ```
1196
1252
 
1197
1253
  The slot receives the cursor in every unit (`step`, `totalSteps`,
@@ -1200,6 +1256,23 @@ and the ones that ran inside its scope (`node`, `relatedNodes`), the `recorder`
1200
1256
  for anything else, and `onNavigate` — the same funnel every built-in mover uses,
1201
1257
  so your pane moves the ONE cursor rather than starting a second one.
1202
1258
 
1259
+ | key | what it does |
1260
+ |---|---|
1261
+ | `detail` | your pane, as the rail's first tab, selected by default |
1262
+ | `detailLabel` | what that tab is CALLED. Default `"Details"` — the library's generic word for someone else's pane. Name it and the strip reads in your reader's words. |
1263
+ | `detailOnly` | opt OUT of the strip: your pane takes the whole rail, as it did before 0.50.0. For a full-height custom layout that would read as a second chrome under the library's strip. The cost: nothing the rail gains later can reach you. |
1264
+
1265
+ **Switching tabs moves nothing and remounts nothing.** The cursor stays where it
1266
+ was, and your pane is hidden rather than unmounted — a reader who glances at
1267
+ *Served* and comes back finds their bands open and their scroll where they left
1268
+ it.
1269
+
1270
+ **The worked example (0.50.0).** An app supplies `detail` + `detailLabel` and
1271
+ changes nothing else: it sees its own pane first and selected, exactly as before,
1272
+ and it GAINS Served, the Served graph and Bookmarks on the version bump. Before
1273
+ 0.50.0 that same app lost the entire rail — the tab strip included — and could
1274
+ not reach any of them.
1275
+
1203
1276
  Keep the slots object stable across renders (module scope or `useMemo`), same as
1204
1277
  `<TraceExplorerShell slots>`.
1205
1278
 
@@ -1333,7 +1406,7 @@ graph in one call. Returns an unsubscribe. Call it once per run.
1333
1406
  | `step` | `number?` | Controlled cursor. **Omit it and the lens is self-driving, exactly as before.** Pass it and you own the position; out-of-range values are clamped and reported. See [Driving the cursor](#driving-the-cursor-from-your-app). |
1334
1407
  | `onStepChange` | `(step, at) => void?` | Fires on every cursor move — required for movement in controlled mode, an observation hook otherwise. `at` carries `runtimeStageId`, `commitIdx`, `label`, `kind` and `clamped`. |
1335
1408
  | `navigatorRef` | `Ref<LensNavigator>?` | Move the cursor to a stage **by its `runtimeStageId`**. `ref.current.navigateTo(id)` returns `{ ok: true, step, match, label }` or `{ ok: false, reason, message, nearest? }` — a miss never moves. See [Pointing at a step](#pointing-at-a-step-navigatorref). |
1336
- | `slots` | `LensSlots?` | Slot overrides. `slots.detail` renders your content in the shipped right column. Omit for the built-in timeline. See [Rendering your own detail pane](#rendering-your-own-detail-pane). |
1409
+ | `slots` | `LensSlots?` | Slot overrides. `slots.detail` renders your content as the right rail's FIRST TAB (`slots.detailLabel` names it; `slots.detailOnly` takes the whole rail instead). The strip stays the library's. Omit for the built-in timeline. See [Rendering your own detail pane](#rendering-your-own-detail-pane). |
1337
1410
 
1338
1411
  ### `<LensFlow>` — the chart canvas on its own
1339
1412
 
@@ -634,16 +634,67 @@ interface LensProps {
634
634
  *
635
635
  * Same idea as `<TraceExplorerShell slots>`: keep the shipped layout and the
636
636
  * shipped cursor, replace what one pane renders.
637
+ *
638
+ * THE LAW: **a slot fills a PANE, never the CHROME around it.** The rail's tab
639
+ * strip, the one cursor and the collapse pill belong to the library, because
640
+ * chrome is how the library adds capability over time — a consumer that fills a
641
+ * slot must still receive every capability the rail gains later. That is why
642
+ * `detail` is ONE TAB and not a takeover, and why `detailOnly` is an opt-out
643
+ * rather than the default.
637
644
  */
638
645
  interface LensSlots {
639
646
  /**
640
- * Override the RIGHT column — the "what happened" timeline. Your component
641
- * renders inside the shipped column (its width, borders, collapse pill and
642
- * scroll behaviour are unchanged); only the content is yours.
647
+ * Render the right column's FIRST TAB — your own pane, beside the library's.
648
+ *
649
+ * Your component renders inside the shipped column (its width, borders,
650
+ * collapse pill and scroll behaviour are unchanged) and inside the shipped
651
+ * rail: the tab strip stays, your pane becomes the first tab and is selected
652
+ * by default, and "What happened", "Served" and "Bookmarks" stay reachable
653
+ * beside it. Switching tabs never moves the cursor and never unmounts your
654
+ * pane — it keeps its own state.
655
+ *
656
+ * Before 0.50.0 a supplied slot replaced the whole rail, tab strip included,
657
+ * so a slot consumer could not reach anything the rail gained afterwards.
658
+ * That was the defect this shape fixes; `detailOnly` is the way back.
643
659
  *
644
660
  * Omit it and the built-in timeline renders exactly as before.
661
+ *
662
+ * ```tsx
663
+ * <Lens recorder={recorder} slots={{ detail: MyBands, detailLabel: 'Bands' }} />
664
+ * // rail: [ Bands* | What happened | Served | Bookmarks ]
665
+ * ```
645
666
  */
646
667
  readonly detail?: React__default.ComponentType<LensDetailSlotProps>;
668
+ /**
669
+ * What the `detail` tab is CALLED. Default: `"Details"`.
670
+ *
671
+ * WHY it exists: the tab strip names each reading in the reader's own words,
672
+ * and "Details" is the library's generic word for someone else's pane. An app
673
+ * whose pane is a stack of SEO bands should say so — the strip is read left to
674
+ * right and an unnamed tab makes the app's own reading the anonymous one.
675
+ *
676
+ * A plain string, rendered as the tab's text; it is the accessible name too.
677
+ * Ignored when no `detail` slot is supplied.
678
+ *
679
+ * ```tsx
680
+ * slots={{ detail: MyBands, detailLabel: 'SEO bands' }}
681
+ * ```
682
+ */
683
+ readonly detailLabel?: string;
684
+ /**
685
+ * OPT OUT of the tab strip: the `detail` slot takes the whole rail, as it did
686
+ * before 0.50.0. Default `false`.
687
+ *
688
+ * For the app with a full-height custom layout of its own — a pane that owns
689
+ * its scroll and its header and would read as a second chrome under the
690
+ * library's strip. The cost is stated plainly: nothing the rail gains later
691
+ * (Served, Bookmarks, whatever follows) can reach that consumer, and neither
692
+ * can the library's own timeline. Prefer the tabs; take this only when the
693
+ * layout genuinely demands it.
694
+ *
695
+ * Ignored when no `detail` slot is supplied.
696
+ */
697
+ readonly detailOnly?: boolean;
647
698
  }
648
699
  /**
649
700
  * What the `detail` slot receives — the cursor, in the units a detail pane
@@ -634,16 +634,67 @@ interface LensProps {
634
634
  *
635
635
  * Same idea as `<TraceExplorerShell slots>`: keep the shipped layout and the
636
636
  * shipped cursor, replace what one pane renders.
637
+ *
638
+ * THE LAW: **a slot fills a PANE, never the CHROME around it.** The rail's tab
639
+ * strip, the one cursor and the collapse pill belong to the library, because
640
+ * chrome is how the library adds capability over time — a consumer that fills a
641
+ * slot must still receive every capability the rail gains later. That is why
642
+ * `detail` is ONE TAB and not a takeover, and why `detailOnly` is an opt-out
643
+ * rather than the default.
637
644
  */
638
645
  interface LensSlots {
639
646
  /**
640
- * Override the RIGHT column — the "what happened" timeline. Your component
641
- * renders inside the shipped column (its width, borders, collapse pill and
642
- * scroll behaviour are unchanged); only the content is yours.
647
+ * Render the right column's FIRST TAB — your own pane, beside the library's.
648
+ *
649
+ * Your component renders inside the shipped column (its width, borders,
650
+ * collapse pill and scroll behaviour are unchanged) and inside the shipped
651
+ * rail: the tab strip stays, your pane becomes the first tab and is selected
652
+ * by default, and "What happened", "Served" and "Bookmarks" stay reachable
653
+ * beside it. Switching tabs never moves the cursor and never unmounts your
654
+ * pane — it keeps its own state.
655
+ *
656
+ * Before 0.50.0 a supplied slot replaced the whole rail, tab strip included,
657
+ * so a slot consumer could not reach anything the rail gained afterwards.
658
+ * That was the defect this shape fixes; `detailOnly` is the way back.
643
659
  *
644
660
  * Omit it and the built-in timeline renders exactly as before.
661
+ *
662
+ * ```tsx
663
+ * <Lens recorder={recorder} slots={{ detail: MyBands, detailLabel: 'Bands' }} />
664
+ * // rail: [ Bands* | What happened | Served | Bookmarks ]
665
+ * ```
645
666
  */
646
667
  readonly detail?: React__default.ComponentType<LensDetailSlotProps>;
668
+ /**
669
+ * What the `detail` tab is CALLED. Default: `"Details"`.
670
+ *
671
+ * WHY it exists: the tab strip names each reading in the reader's own words,
672
+ * and "Details" is the library's generic word for someone else's pane. An app
673
+ * whose pane is a stack of SEO bands should say so — the strip is read left to
674
+ * right and an unnamed tab makes the app's own reading the anonymous one.
675
+ *
676
+ * A plain string, rendered as the tab's text; it is the accessible name too.
677
+ * Ignored when no `detail` slot is supplied.
678
+ *
679
+ * ```tsx
680
+ * slots={{ detail: MyBands, detailLabel: 'SEO bands' }}
681
+ * ```
682
+ */
683
+ readonly detailLabel?: string;
684
+ /**
685
+ * OPT OUT of the tab strip: the `detail` slot takes the whole rail, as it did
686
+ * before 0.50.0. Default `false`.
687
+ *
688
+ * For the app with a full-height custom layout of its own — a pane that owns
689
+ * its scroll and its header and would read as a second chrome under the
690
+ * library's strip. The cost is stated plainly: nothing the rail gains later
691
+ * (Served, Bookmarks, whatever follows) can reach that consumer, and neither
692
+ * can the library's own timeline. Prefer the tabs; take this only when the
693
+ * layout genuinely demands it.
694
+ *
695
+ * Ignored when no `detail` slot is supplied.
696
+ */
697
+ readonly detailOnly?: boolean;
647
698
  }
648
699
  /**
649
700
  * What the `detail` slot receives — the cursor, in the units a detail pane
@@ -3296,16 +3296,16 @@ function diffPromptsBounded(a, b, maxCells = DIFF_CELL_CAP) {
3296
3296
  }
3297
3297
 
3298
3298
  // src/core/served/sincePrevious.ts
3299
- function unmatched(a, b) {
3299
+ function unmatchedIndexes(a, b) {
3300
3300
  const pool = /* @__PURE__ */ new Map();
3301
3301
  for (const item of b) pool.set(item, (pool.get(item) ?? 0) + 1);
3302
- let count = 0;
3303
- for (const item of a) {
3302
+ const out = [];
3303
+ for (const [i, item] of a.entries()) {
3304
3304
  const n = pool.get(item) ?? 0;
3305
3305
  if (n > 0) pool.set(item, n - 1);
3306
- else count += 1;
3306
+ else out.push(i);
3307
3307
  }
3308
- return count;
3308
+ return out;
3309
3309
  }
3310
3310
  function sincePrevious(current, previous) {
3311
3311
  const now = current.view;
@@ -3328,6 +3328,10 @@ function sincePrevious(current, previous) {
3328
3328
  if (a !== void 0 && b !== void 0 && a !== b) schemaChanged.push(name);
3329
3329
  }
3330
3330
  }
3331
+ const piecesEntered = unmatchedIndexes(nowPieces, beforePieces);
3332
+ const piecesLeft = unmatchedIndexes(beforePieces, nowPieces);
3333
+ const messagesEntered = unmatchedIndexes(nowMessages, beforeMessages);
3334
+ const messagesLeft = unmatchedIndexes(beforeMessages, nowMessages);
3331
3335
  const changed = now.system.text !== before.system.text;
3332
3336
  const diff = changed ? diffPromptsBounded(before.system.text, now.system.text) : Object.freeze([]);
3333
3337
  return Object.freeze({
@@ -3336,12 +3340,16 @@ function sincePrevious(current, previous) {
3336
3340
  system: Object.freeze({
3337
3341
  changed,
3338
3342
  ...diff !== void 0 ? { diff: Object.freeze([...diff]) } : {},
3339
- piecesEntered: unmatched(nowPieces, beforePieces),
3340
- piecesLeft: unmatched(beforePieces, nowPieces)
3343
+ piecesEntered: piecesEntered.length,
3344
+ piecesLeft: piecesLeft.length,
3345
+ enteredIndexes: Object.freeze(piecesEntered),
3346
+ leftPieces: Object.freeze(piecesLeft.map((i) => before.system.pieces[i]))
3341
3347
  }),
3342
3348
  messages: Object.freeze({
3343
- entered: unmatched(nowMessages, beforeMessages),
3344
- left: unmatched(beforeMessages, nowMessages)
3349
+ entered: messagesEntered.length,
3350
+ left: messagesLeft.length,
3351
+ enteredIndexes: Object.freeze(messagesEntered),
3352
+ leftEntries: Object.freeze(messagesLeft.map((i) => before.messages.asSent[i]))
3345
3353
  }),
3346
3354
  tools: Object.freeze({
3347
3355
  added: Object.freeze(added),
@@ -3404,6 +3412,295 @@ function foldFactsAt(recording, cursor) {
3404
3412
  });
3405
3413
  }
3406
3414
 
3415
+ // src/core/served/servedGraphAt.ts
3416
+ import {
3417
+ SERVED_GAPS as SERVED_GAPS2,
3418
+ UNGAPPED_FIELDS
3419
+ } from "agentfootprint";
3420
+ var SLOT_ORDER = Object.freeze({
3421
+ "system-prompt": 0,
3422
+ messages: 1,
3423
+ tools: 2
3424
+ });
3425
+ var SERVED_SLOTS = Object.freeze(
3426
+ Object.keys(SLOT_ORDER).sort((a, b) => SLOT_ORDER[a] - SLOT_ORDER[b])
3427
+ );
3428
+ var HELD_HONESTY_KEYS = Object.freeze([
3429
+ "basis",
3430
+ "redacted",
3431
+ "skipped",
3432
+ "foldError"
3433
+ ]);
3434
+ var HELD_KEYS = Object.freeze([
3435
+ ...FOLD_FACT_KEYS,
3436
+ ...HELD_HONESTY_KEYS
3437
+ ]);
3438
+ var RECONSTRUCTED = Object.freeze({ status: "reconstructed" });
3439
+ var NOT_ON_RECORD = Object.freeze({ status: "not-on-record" });
3440
+ function slotsOfFields(fields) {
3441
+ const out = [];
3442
+ for (const field of fields) {
3443
+ const slot = field.startsWith("system.") ? "system-prompt" : field.startsWith("messages.") ? "messages" : field.startsWith("tools.") ? "tools" : void 0;
3444
+ if (slot !== void 0 && !out.includes(slot)) out.push(slot);
3445
+ }
3446
+ return out;
3447
+ }
3448
+ function heldNodes(fold) {
3449
+ const node = (key, value, status) => Object.freeze(value === void 0 ? { key, status } : { key, value, status });
3450
+ const out = [];
3451
+ for (const key of FOLD_FACT_KEYS) {
3452
+ const value = fold[key];
3453
+ out.push(value === void 0 ? node(key, void 0, "not-on-record") : node(key, value, "reconstructed"));
3454
+ }
3455
+ out.push(
3456
+ fold.basis === void 0 ? node("basis", void 0, "not-on-record") : node("basis", fold.basis, "reconstructed")
3457
+ );
3458
+ out.push(node("redacted", fold.redacted, "reconstructed"));
3459
+ if (fold.skipped !== void 0) out.push(node("skipped", fold.skipped, "damaged"));
3460
+ if (fold.foldError !== void 0) out.push(node("foldError", fold.foldError, "damaged"));
3461
+ return Object.freeze(out);
3462
+ }
3463
+ function servedEdges(input) {
3464
+ const { row, checks, since } = input;
3465
+ const view = row.view;
3466
+ const edge = (e) => Object.freeze(e);
3467
+ const out = [];
3468
+ const compared = since !== void 0;
3469
+ const enteredPieces = new Set(since?.system.enteredIndexes ?? []);
3470
+ const enteredMessages = new Set(since?.messages.enteredIndexes ?? []);
3471
+ const stateOf = (entered) => compared ? entered ? "entered" : "unchanged" : void 0;
3472
+ const withState = (state) => state !== void 0 ? { state } : {};
3473
+ view.system.pieces.forEach((piece, i) => {
3474
+ out.push(
3475
+ edge({
3476
+ id: `piece:${i}`,
3477
+ kind: "piece",
3478
+ slot: piece.slot,
3479
+ source: piece.source,
3480
+ origin: piece.source,
3481
+ text: piece.text,
3482
+ check: checks.pieces[i] ?? RECONSTRUCTED,
3483
+ ...withState(stateOf(enteredPieces.has(i)))
3484
+ })
3485
+ );
3486
+ });
3487
+ (since?.system.leftPieces ?? []).forEach((piece, i) => {
3488
+ out.push(
3489
+ edge({
3490
+ id: `piece-left:${i}`,
3491
+ kind: "piece",
3492
+ slot: piece.slot,
3493
+ source: piece.source,
3494
+ origin: piece.source,
3495
+ text: piece.text,
3496
+ // Not on this request at all: there is no row here for a hash to check.
3497
+ check: NOT_ON_RECORD,
3498
+ state: "left"
3499
+ })
3500
+ );
3501
+ });
3502
+ view.messages.asSent.forEach((message, i) => {
3503
+ const label = message.toolName ?? message.toolCallId;
3504
+ out.push(
3505
+ edge({
3506
+ id: `message:${i}`,
3507
+ kind: "message",
3508
+ slot: "messages",
3509
+ role: message.role,
3510
+ origin: message.role,
3511
+ ...label !== void 0 ? { label } : {},
3512
+ text: message.content,
3513
+ check: checks.messages[i] ?? RECONSTRUCTED,
3514
+ ...withState(stateOf(enteredMessages.has(i)))
3515
+ })
3516
+ );
3517
+ });
3518
+ (since?.messages.leftEntries ?? []).forEach((message, i) => {
3519
+ out.push(
3520
+ edge({
3521
+ id: `message-left:${i}`,
3522
+ kind: "message",
3523
+ slot: "messages",
3524
+ role: message.role,
3525
+ origin: message.role,
3526
+ text: message.content,
3527
+ check: NOT_ON_RECORD,
3528
+ state: "left"
3529
+ })
3530
+ );
3531
+ });
3532
+ view.messages.requestOnly.forEach((line, i) => {
3533
+ out.push(
3534
+ edge({
3535
+ id: `request-only:${i}`,
3536
+ kind: "request-only",
3537
+ slot: "messages",
3538
+ role: line.role,
3539
+ origin: line.role,
3540
+ label: line.reason,
3541
+ text: line.text,
3542
+ check: checks.requestOnly[i] ?? RECONSTRUCTED
3543
+ })
3544
+ );
3545
+ });
3546
+ view.tools.names.forEach((name) => {
3547
+ const schema = view.tools.schemas.find((s) => s.name === name);
3548
+ const state = compared ? since !== void 0 && since.tools.added.includes(name) ? "entered" : "unchanged" : void 0;
3549
+ out.push(
3550
+ edge({
3551
+ id: `tool:${name}`,
3552
+ kind: "tool",
3553
+ slot: "tools",
3554
+ label: name,
3555
+ // No rebuilt schema means no hash on this side; a NAME is never
3556
+ // verified — the receipt carries names as plain strings.
3557
+ check: schema !== void 0 ? checks.toolSchemas[name] ?? RECONSTRUCTED : RECONSTRUCTED,
3558
+ ...view.tools.forced === name ? { forced: true } : {},
3559
+ ...checks.onReceiptOnly.schemas.includes(name) ? { schemaOnReceiptOnly: true } : {},
3560
+ ...since !== void 0 && since.tools.schemaChanged.includes(name) ? { schemaChanged: true } : {},
3561
+ ...withState(state)
3562
+ })
3563
+ );
3564
+ });
3565
+ (since?.tools.removed ?? []).forEach((name) => {
3566
+ out.push(
3567
+ edge({
3568
+ id: `tool-left:${name}`,
3569
+ kind: "tool",
3570
+ slot: "tools",
3571
+ label: name,
3572
+ check: NOT_ON_RECORD,
3573
+ state: "left"
3574
+ })
3575
+ );
3576
+ });
3577
+ return Object.freeze(out);
3578
+ }
3579
+ function slotNodes(input, edges) {
3580
+ const { row, checks } = input;
3581
+ const view = row.view;
3582
+ const receipt = row.receipt;
3583
+ const gapsFor = (slot) => Object.freeze(view.gaps.filter((g) => slotsOfFields(g.fields).includes(slot)));
3584
+ const rebuiltIn = (slot) => edges.filter((e) => e.slot === slot && e.state !== "left").length;
3585
+ const onReceipt = {
3586
+ "system-prompt": receipt?.system.pieces.length,
3587
+ messages: receipt !== void 0 ? receipt.messages.entries.length + receipt.messages.requestOnly.length : void 0,
3588
+ tools: checks.namesOnReceipt?.length
3589
+ };
3590
+ const check2 = {
3591
+ "system-prompt": checks.system,
3592
+ messages: void 0,
3593
+ tools: checks.toolNames
3594
+ };
3595
+ const onReceiptOnly = {
3596
+ "system-prompt": checks.onReceiptOnly.pieces,
3597
+ messages: checks.onReceiptOnly.messages + checks.onReceiptOnly.requestOnly,
3598
+ tools: checks.onReceiptOnly.toolNames.length
3599
+ };
3600
+ const rebuiltOnly = {
3601
+ "system-prompt": checks.rebuiltOnly.pieces,
3602
+ messages: checks.rebuiltOnly.messages + checks.rebuiltOnly.requestOnly,
3603
+ tools: checks.rebuiltOnly.toolNames.length
3604
+ };
3605
+ return Object.freeze(
3606
+ SERVED_SLOTS.map((slot) => {
3607
+ const aggregate = check2[slot];
3608
+ const count = onReceipt[slot];
3609
+ return Object.freeze({
3610
+ slot,
3611
+ ...aggregate !== void 0 ? { check: aggregate } : {},
3612
+ rebuilt: rebuiltIn(slot),
3613
+ ...count !== void 0 ? { onReceipt: count } : {},
3614
+ onReceiptOnly: onReceiptOnly[slot],
3615
+ rebuiltOnly: rebuiltOnly[slot],
3616
+ gaps: gapsFor(slot)
3617
+ });
3618
+ })
3619
+ );
3620
+ }
3621
+ function withheldNodes(input) {
3622
+ const { row, fold } = input;
3623
+ const view = row.view;
3624
+ const out = [];
3625
+ if (view.tools.withheld !== void 0) {
3626
+ out.push(
3627
+ Object.freeze({
3628
+ kind: "tool-withheld",
3629
+ name: view.tools.withheld,
3630
+ from: "view",
3631
+ slot: "tools"
3632
+ })
3633
+ );
3634
+ }
3635
+ for (const id of fold.hiddenSkillIds ?? []) {
3636
+ out.push(Object.freeze({ kind: "hidden-skill", name: id, from: "fold" }));
3637
+ }
3638
+ const drops = row.receipt?.omittedForAttention;
3639
+ out.push(
3640
+ Object.freeze(
3641
+ drops !== void 0 ? {
3642
+ kind: "attention-drop",
3643
+ name: "omittedForAttention",
3644
+ from: "receipt",
3645
+ count: drops.count,
3646
+ hashes: Object.freeze([...drops.hashes])
3647
+ } : {
3648
+ // Absent is not none: the library's own sentence for a field no gap
3649
+ // explains, printed beside the Not-on-record badge.
3650
+ kind: "attention-drop",
3651
+ name: "omittedForAttention",
3652
+ from: "receipt",
3653
+ status: "not-on-record",
3654
+ why: UNGAPPED_FIELDS.omittedForAttention
3655
+ }
3656
+ )
3657
+ );
3658
+ if (fold.redacted) {
3659
+ out.push(Object.freeze({ kind: "redacted", name: "redacted", from: "fold" }));
3660
+ }
3661
+ for (const gap of view.gaps) {
3662
+ const slots = slotsOfFields(gap.fields);
3663
+ out.push(
3664
+ Object.freeze({
3665
+ kind: "gap",
3666
+ name: gap.gap,
3667
+ from: "view",
3668
+ // One slot named: draw it there. Several, or none: the band itself.
3669
+ ...slots.length === 1 ? { slot: slots[0] } : {},
3670
+ why: SERVED_GAPS2[gap.gap].why,
3671
+ fields: gap.fields,
3672
+ ...gap.cause !== void 0 ? { cause: gap.cause } : {}
3673
+ })
3674
+ );
3675
+ }
3676
+ return Object.freeze(out);
3677
+ }
3678
+ function callNode(input, edges) {
3679
+ const { row, checks } = input;
3680
+ const basis = row.view.basis;
3681
+ return Object.freeze({
3682
+ epoch: row.epoch,
3683
+ callRuntimeStageId: row.callRuntimeStageId,
3684
+ commitIdx: row.commitIdx,
3685
+ betweenCalls: row.betweenCalls,
3686
+ ...row.previousEpoch !== void 0 ? { previousEpoch: row.previousEpoch } : {},
3687
+ ...basis !== void 0 ? { basis } : {},
3688
+ basisStatus: basis !== void 0 ? "reconstructed" : "not-on-record",
3689
+ ...row.receiptCause !== void 0 ? { receiptCause: row.receiptCause } : {},
3690
+ damaged: checks.damaged || edges.some((e) => e.check.status === "damaged")
3691
+ });
3692
+ }
3693
+ function servedGraphAt(input) {
3694
+ const edges = servedEdges(input);
3695
+ return Object.freeze({
3696
+ held: heldNodes(input.fold),
3697
+ served: slotNodes(input, edges),
3698
+ withheld: withheldNodes(input),
3699
+ call: callNode(input, edges),
3700
+ edges
3701
+ });
3702
+ }
3703
+
3407
3704
  // src/core/bookmarks/bookmarkKey.ts
3408
3705
  function fnv1a(s) {
3409
3706
  let h = 2166136261;
@@ -3783,6 +4080,10 @@ export {
3783
4080
  sincePrevious,
3784
4081
  FOLD_FACT_KEYS,
3785
4082
  foldFactsAt,
4083
+ SERVED_SLOTS,
4084
+ HELD_HONESTY_KEYS,
4085
+ HELD_KEYS,
4086
+ servedGraphAt,
3786
4087
  bookmarkKey,
3787
4088
  snapshotOfRunner,
3788
4089
  snapshotLogKey,
@@ -3799,4 +4100,4 @@ export {
3799
4100
  tagStopsFor,
3800
4101
  tagAxisPositions
3801
4102
  };
3802
- //# sourceMappingURL=chunk-FZHRQNRS.js.map
4103
+ //# sourceMappingURL=chunk-AIZIILHJ.js.map