@basou/cli 0.74.0 → 0.74.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/dist/index.js CHANGED
@@ -863,6 +863,7 @@ import {
863
863
  readPreviousRecords,
864
864
  recordIds,
865
865
  resolveViewLanguage,
866
+ setEntryOf,
866
867
  shellWord,
867
868
  writeRecord
868
869
  } from "@basou/core";
@@ -1082,6 +1083,18 @@ var SHORT_ID_LEN2 = 6;
1082
1083
  function collectPath(value, previous) {
1083
1084
  return [...previous, value];
1084
1085
  }
1086
+ function reimportFallText(change) {
1087
+ const parts = [];
1088
+ if (change.decisions < 0) {
1089
+ const n = -change.decisions;
1090
+ parts.push(`${n.toLocaleString("en-US")} fewer decision${n === 1 ? "" : "s"}`);
1091
+ }
1092
+ if (change.outputTokens < 0) {
1093
+ const n = -change.outputTokens;
1094
+ parts.push(`${n.toLocaleString("en-US")} fewer output token${n === 1 ? "" : "s"}`);
1095
+ }
1096
+ return parts.length === 0 ? null : parts.join(", ");
1097
+ }
1085
1098
  function registerImportCommand(program2) {
1086
1099
  const importCmd = program2.command("import").description("Import provenance from an external AI tool's native logs");
1087
1100
  importCmd.command("claude-code").description("Derive Basou sessions from Claude Code native transcripts (~/.claude/projects)").option(
@@ -1193,7 +1206,8 @@ async function doRunImportClaudeCode(options, ctx) {
1193
1206
  store,
1194
1207
  candidates,
1195
1208
  projectPaths,
1196
- hasDeclaredBoundary(options, manifest)
1209
+ hasDeclaredBoundary(options, manifest),
1210
+ ctx.onReimportChange
1197
1211
  );
1198
1212
  }
1199
1213
  async function doRunImportCodex(options, ctx) {
@@ -1229,7 +1243,8 @@ async function doRunImportCodex(options, ctx) {
1229
1243
  await loadImportStore(paths, options, CODEX_IMPORT_SOURCE),
1230
1244
  candidates,
1231
1245
  projectPaths,
1232
- hasDeclaredBoundary(options, manifest)
1246
+ hasDeclaredBoundary(options, manifest),
1247
+ ctx.onReimportChange
1233
1248
  );
1234
1249
  }
1235
1250
  function hasDeclaredBoundary(options, manifest) {
@@ -1251,7 +1266,7 @@ async function resolveImportTarget(ctx) {
1251
1266
  const manifest = await readManifest2(paths);
1252
1267
  return { repositoryRoot, paths, manifest };
1253
1268
  }
1254
- async function importDerivedSessions(paths, manifest, options, store, candidates, projectPaths, boundaryDeclared) {
1269
+ async function importDerivedSessions(paths, manifest, options, store, candidates, projectPaths, boundaryDeclared, onReimportChange) {
1255
1270
  const { byExternalId: existingByExternalId, notDirectories, yamlNotFiles } = store;
1256
1271
  let notDirectoriesWarned = false;
1257
1272
  const seenThisRun = /* @__PURE__ */ new Set();
@@ -1281,7 +1296,8 @@ async function importDerivedSessions(paths, manifest, options, store, candidates
1281
1296
  skippedDecreased: 0,
1282
1297
  skippedDuplicate: 0,
1283
1298
  skippedUnverifiable: 0,
1284
- droppedCopies: 0
1299
+ droppedCopies: 0,
1300
+ change: { decisions: 0, outputTokens: 0 }
1285
1301
  };
1286
1302
  let sanitizedPaths = 0;
1287
1303
  const validate = (payload) => {
@@ -1343,6 +1359,8 @@ async function importDerivedSessions(paths, manifest, options, store, candidates
1343
1359
  continue;
1344
1360
  }
1345
1361
  counts.reimported++;
1362
+ counts.change.decisions += outcome.change.decisions;
1363
+ counts.change.outputTokens += outcome.change.outputTokens;
1346
1364
  if (holdsCopies?.(prior) === true) counts.droppedCopies++;
1347
1365
  seenThisRun.add(externalId);
1348
1366
  await noteCrossProject(externalId, payload2);
@@ -1397,6 +1415,7 @@ async function importDerivedSessions(paths, manifest, options, store, candidates
1397
1415
  );
1398
1416
  }
1399
1417
  }
1418
+ onReimportChange?.({ ...counts.change });
1400
1419
  printImportResult(options, results, counts);
1401
1420
  }
1402
1421
  async function classifyReimport(priors, sourcePath, externalId, counts, holdsCopies) {
@@ -1849,7 +1868,9 @@ function printImportResult(options, results, counts) {
1849
1868
  skipParts.push(`${skippedUnverifiable} unverifiable (run 'basou verify')`);
1850
1869
  const skipSuffix = skipParts.length > 0 ? `; skipped ${skipParts.join(", ")}` : "";
1851
1870
  const eventsPart = replaced > 0 ? `${eventTotal} events, ${replaced} replaced` : `${eventTotal} events`;
1852
- const copiesPart = droppedCopies > 0 ? ` (${droppedCopies} without the records ${droppedCopies === 1 ? "its transcript" : "their transcripts"} copied from an older one)` : "";
1871
+ const copies = droppedCopies > 0 ? `${droppedCopies} without the records ${droppedCopies === 1 ? "its transcript" : "their transcripts"} copied from an older one` : null;
1872
+ const notes = [copies, reimportFallText(counts.change)].filter((n) => n !== null);
1873
+ const copiesPart = notes.length > 0 ? ` (${notes.join("; ")})` : "";
1853
1874
  if (isDry) {
1854
1875
  const parts = [];
1855
1876
  if (results.length > 0) parts.push(`import ${results.length} session(s) (${eventsPart})`);
@@ -1948,10 +1969,23 @@ function isMissingSourceDir(error) {
1948
1969
  if (!(error instanceof Error)) return false;
1949
1970
  return error.message === "Claude transcript directory not found for project" || error.message === "Codex sessions directory not found";
1950
1971
  }
1972
+ var reimportChanges = /* @__PURE__ */ new WeakMap();
1973
+ function reimportChangeOf(outcome) {
1974
+ return reimportChanges.get(outcome);
1975
+ }
1976
+ function noteReimportChange(outcome, change) {
1977
+ reimportChanges.set(outcome, change);
1978
+ return outcome;
1979
+ }
1951
1980
  async function runImport(adapter, fn) {
1952
1981
  try {
1953
- const json = await captureImportJson(fn);
1954
- return {
1982
+ let reimportChange = { decisions: 0, outputTokens: 0 };
1983
+ const json = await captureImportJson(
1984
+ () => fn((change) => {
1985
+ reimportChange = change;
1986
+ })
1987
+ );
1988
+ const outcome = {
1955
1989
  adapter,
1956
1990
  status: "ran",
1957
1991
  importedCount: readCount(json.imported_count),
@@ -1967,6 +2001,7 @@ async function runImport(adapter, fn) {
1967
2001
  eventTotal: readCount(json.event_total),
1968
2002
  dryRun: json.dry_run === true
1969
2003
  };
2004
+ return noteReimportChange(outcome, reimportChange);
1970
2005
  } catch (error) {
1971
2006
  if (isMissingSourceDir(error)) {
1972
2007
  return { adapter, status: "skipped", reason: "no source logs for this project" };
@@ -1984,10 +2019,16 @@ function importOptions(options) {
1984
2019
  };
1985
2020
  }
1986
2021
  function importClaudeCode(options, ctx) {
1987
- return runImport("claude-code", () => doRunImportClaudeCode(importOptions(options), ctx));
2022
+ return runImport(
2023
+ "claude-code",
2024
+ (onReimportChange) => doRunImportClaudeCode(importOptions(options), { ...ctx, onReimportChange })
2025
+ );
1988
2026
  }
1989
2027
  function importCodex(options, ctx) {
1990
- return runImport("codex", () => doRunImportCodex(importOptions(options), ctx));
2028
+ return runImport(
2029
+ "codex",
2030
+ (onReimportChange) => doRunImportCodex(importOptions(options), { ...ctx, onReimportChange })
2031
+ );
1991
2032
  }
1992
2033
  async function regenerateHandoff(paths, nowIso, callbacks) {
1993
2034
  const result = await renderHandoff({ paths, nowIso, ...callbacks });
@@ -2811,11 +2852,14 @@ function diffLines(diff, more) {
2811
2852
  ` ${displayPath3(c.at)}: ${shownJson(c.before)} -> ${shownJson(c.after)}${delta}${flag(c)}`
2812
2853
  );
2813
2854
  }
2814
- for (const c of diff.added)
2815
- lines.push(` + ${displayPath3(c.at)}: ${shownJson(c.value)}${flag(c)}`);
2816
- for (const c of diff.removed) {
2817
- lines.push(` - ${displayPath3(c.at)}: ${shownJson(c.value)}${flag(c)}`);
2818
- }
2855
+ const only = (sign, c) => {
2856
+ const entry = setEntryOf(c.at);
2857
+ if (entry === null) return ` ${sign} ${displayPath3(c.at)}: ${shownJson(c.value)}${flag(c)}`;
2858
+ const how = sign === "+" ? "now lists" : "no longer lists";
2859
+ return ` ${sign} ${displayPath3(entry.list)}: ${how} ${shownJson(entry.name)}${flag(c)}`;
2860
+ };
2861
+ for (const c of diff.added) lines.push(only("+", c));
2862
+ for (const c of diff.removed) lines.push(only("-", c));
2819
2863
  if (lines.length === 0 && !more) lines.push(" nothing moved");
2820
2864
  return lines;
2821
2865
  }
@@ -10231,8 +10275,13 @@ function describeImport(outcome) {
10231
10275
  const verb = outcome.dryRun ? "would import" : "imported";
10232
10276
  const parts = [`${outcome.importedCount} session(s)`, `${outcome.eventTotal} events`];
10233
10277
  if (outcome.reimportedCount > 0) {
10234
- const copies = outcome.droppedCopiesCount > 0 ? ` (${outcome.droppedCopiesCount} without records copied from an older transcript)` : "";
10235
- parts.push(`${outcome.reimportedCount} re-imported${copies}`);
10278
+ const notes = [
10279
+ outcome.droppedCopiesCount > 0 ? `${outcome.droppedCopiesCount} without records copied from an older transcript` : null,
10280
+ reimportFallText(reimportChangeOf(outcome) ?? { decisions: 0, outputTokens: 0 })
10281
+ ].filter((n) => n !== null);
10282
+ parts.push(
10283
+ `${outcome.reimportedCount} re-imported${notes.length > 0 ? ` (${notes.join("; ")})` : ""}`
10284
+ );
10236
10285
  }
10237
10286
  if (outcome.replacedCount > 0) parts.push(`${outcome.replacedCount} replaced`);
10238
10287
  if (outcome.skippedAlreadyImported > 0)
@@ -13966,12 +14015,31 @@ var BOARD_HTML = `<!doctype html>
13966
14015
  table { border-collapse: collapse; }
13967
14016
  th, td { text-align: left; vertical-align: top; padding: 4px 10px 4px 0; }
13968
14017
  td.n, th.n { text-align: right; font-variant-numeric: tabular-nums; }
14018
+ /* A number is never broken across lines, so a part of it cannot pass for another number. */
14019
+ td.n { white-space: nowrap; }
13969
14020
  table.grid td, table.grid th { border-bottom: 1px solid var(--grid); }
13970
- .scroll { overflow-x: auto; max-width: 100%; }
14021
+ /* Where a table is wider than its box, a shade at an edge says there is
14022
+ more that way, also where scrollbars are drawn over the content and
14023
+ hidden: each shade stays at its edge, and a cover of the page's color,
14024
+ moving with the content, hides it while that end is in view. */
14025
+ .scroll {
14026
+ overflow-x: auto; max-width: 100%;
14027
+ background:
14028
+ linear-gradient(to right, var(--bg) 40%, transparent) left center / 28px 100% no-repeat local,
14029
+ linear-gradient(to left, var(--bg) 40%, transparent) right center / 28px 100% no-repeat local,
14030
+ radial-gradient(farthest-side at 0 50%, color-mix(in srgb, var(--fg-2) 40%, transparent), transparent) left center / 12px 100% no-repeat scroll,
14031
+ radial-gradient(farthest-side at 100% 50%, color-mix(in srgb, var(--fg-2) 40%, transparent), transparent) right center / 12px 100% no-repeat scroll;
14032
+ }
14033
+ /* Lane names, and the heads of the repos' table, break between words, not
14034
+ inside one (a name in Japanese is one word). */
14035
+ td.lanes, table.repos th { word-break: keep-all; }
13971
14036
  table.matrix th.stage { font-weight: 400; font-size: 12px; text-align: center; min-width: 64px; }
13972
14037
  table.matrix th.stage b { display: block; font-size: 13px; }
13973
14038
  table.matrix td.cell { text-align: center; font-size: 18px; line-height: 1; }
13974
14039
  .stage-meanings { display: none; list-style: none; padding: 0; margin: 6px 0 0; font-size: 12px; color: var(--fg-2); }
14040
+ /* Drawn only below about 600px, by the rule below. */
14041
+ tr.source { display: none; }
14042
+ tr.source td { padding-top: 0; }
13975
14043
  /* Six stages do not fit beside their meanings below about 600px: each is
13976
14044
  drawn by its id, the meanings are listed under the matrix, and a lane
13977
14045
  name wraps. After the rules above, which it overrides. */
@@ -13987,8 +14055,13 @@ var BOARD_HTML = `<!doctype html>
13987
14055
  is cut off. The cell keeps no right padding, which a sticky note could
13988
14056
  not cross. */
13989
14057
  .scroll { container-type: inline-size; }
13990
- tr.notes td { padding-right: 0; }
13991
- tr.notes ul.sub { position: sticky; left: 0; max-width: calc(100vw - 32px); max-width: 100cqw; }
14058
+ tr.notes td, tr.source td { padding-right: 0; }
14059
+ tr.notes ul.sub, tr.source div.sub { position: sticky; left: 0; max-width: calc(100vw - 32px); max-width: 100cqw; }
14060
+ /* An observation's source is drawn in a row of its own under it, where
14061
+ it wraps at the width in view, not in a column off the screen that
14062
+ makes its row tall. */
14063
+ th.source, td.source { display: none; }
14064
+ tr.source { display: table-row; }
13992
14065
  }
13993
14066
  .st::before { display: inline-block; width: 1.2em; }
13994
14067
  .st.done::before { content: "\\25CF"; color: var(--done); }
@@ -13997,7 +14070,9 @@ var BOARD_HTML = `<!doctype html>
13997
14070
  .st.shelved::before { content: "\\2298"; color: var(--fg-2); }
13998
14071
  .st.none::before { content: "\\25CB"; color: var(--fg-2); }
13999
14072
  .st.unverified::before { content: "?"; color: var(--info); font-weight: 700; }
14000
- td.moved { outline: 2px solid var(--info); outline-offset: -3px; border-radius: 6px; }
14073
+ td.moved, .moved-key { outline: 2px solid var(--info); outline-offset: -3px; border-radius: 6px; }
14074
+ .moved-key { display: inline-block; width: 1.2em; height: 1.2em; margin-right: 4px; vertical-align: -0.3em; }
14075
+ ul.moved { margin: 6px 0 0; padding-left: 18px; font-size: 12px; }
14001
14076
  .legend { font-size: 12px; color: var(--fg-2); display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 6px; }
14002
14077
  .swatch { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 4px; vertical-align: -1px; }
14003
14078
  .swatch.s1 { background: var(--s1); }
@@ -14015,6 +14090,7 @@ var BOARD_HTML = `<!doctype html>
14015
14090
  td.wrap { overflow-wrap: anywhere; min-width: 10em; }
14016
14091
  tr.notes td { padding-top: 0; }
14017
14092
  .chip.changed { margin-left: 0; margin-right: 6px; background: var(--chip-unverified); }
14093
+ .chip.added { margin-left: 0; margin-right: 6px; background: var(--chip-live); }
14018
14094
  .bar { height: 10px; background: var(--fill); border-radius: 5px; min-width: 120px; position: relative; overflow: hidden; }
14019
14095
  .bar span { position: absolute; left: 0; top: 0; bottom: 0; background: var(--s1); }
14020
14096
  figure { margin: 16px 0; }
@@ -14160,11 +14236,27 @@ var BOARD_HTML = `<!doctype html>
14160
14236
  var DOT = ' ' + String.fromCharCode(183) + ' ';
14161
14237
  var FOUND = String.fromCharCode(10003);
14162
14238
  var NOT_FOUND = String.fromCharCode(10007);
14239
+ // Whether a final_url is where the declared url points: the same origin and
14240
+ // path, and nothing more. A final_url written from basou 0.73.0 holds no
14241
+ // query, fragment, user or password; one in a record written before may, and
14242
+ // is then drawn, as what it holds can be what moved. A test lifts this function.
14243
+ function landsAsDeclared(url, finalUrl) {
14244
+ try {
14245
+ var a = new URL(url);
14246
+ var b = new URL(finalUrl);
14247
+ return a.origin === b.origin && a.pathname === b.pathname &&
14248
+ b.search === '' && b.hash === '' && b.username === '' && b.password === '';
14249
+ } catch (e) {
14250
+ return false;
14251
+ }
14252
+ }
14163
14253
  // An http value read as its parts: the status, the redirects, where it
14164
- // landed, and what it read off the body (when the status is 2xx) or that it
14254
+ // landed (left out where it is the url declared, which is drawn under the
14255
+ // name), and what it read off the body (when the status is 2xx) or that it
14165
14256
  // did not. Numbers and strings are drawn as they are.
14166
- function httpText(h) {
14167
- var parts = [String(h.status), fill(S.observed.redirects, { n: h.redirects }), h.final_url];
14257
+ function httpText(h, url) {
14258
+ var parts = [String(h.status), fill(S.observed.redirects, { n: h.redirects })];
14259
+ if (!url || !landsAsDeclared(url, h.final_url)) parts.push(h.final_url);
14168
14260
  if (h.body === null) parts.push(S.observed.bodyNotRead);
14169
14261
  else if (h.body) {
14170
14262
  if (h.body.words) {
@@ -14179,23 +14271,35 @@ var BOARD_HTML = `<!doctype html>
14179
14271
  }
14180
14272
  // A value of an observation as the page draws it: an http value in its
14181
14273
  // shape read as its parts, one with words beside what it says of each, and
14182
- // any other as it is (a string) or as JSON. A test lifts this function.
14183
- function observedText(value, words, http) {
14184
- if (http) return httpText(http);
14274
+ // any other as it is (a string) or as JSON. url: the url its declaration
14275
+ // gives, or null. A test lifts this function.
14276
+ function observedText(value, words, http, url) {
14277
+ if (http) return httpText(http, url);
14185
14278
  if (words) return withWords(value, words);
14186
14279
  return shown(value);
14187
14280
  }
14281
+ // The url an http observation's declaration gives, or null.
14282
+ function declaredUrl(o) {
14283
+ return o.kind === 'http' && o.looks_at && o.looks_at.length > 0 ? o.looks_at[0] : null;
14284
+ }
14285
+ // When an observation was made, as the page draws a time; a day alone (a
14286
+ // manual observation may give one) is drawn as it is, not as its midnight.
14287
+ // A test lifts this function.
14288
+ function observedAtText(at) {
14289
+ return /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(String(at)) ? String(at) : when(at);
14290
+ }
14188
14291
  // The value of an observation that moved, and what the previous record
14189
14292
  // held: a value whose type alone moved ("12" and 12) is drawn as JSON, so
14190
14293
  // that the two do not read the same. A test lifts this function.
14191
14294
  function movedText(o) {
14192
14295
  var m = o.moved;
14193
- var now = observedText(o.value, o.words, o.http);
14296
+ var url = declaredUrl(o);
14297
+ var now = observedText(o.value, o.words, o.http, url);
14194
14298
  if (m.status === 'none') return { now: now, then: S.observed.beforeNone };
14195
14299
  if (m.value === null) {
14196
14300
  return { now: now, then: S.observed.beforeNotObserved + (m.error ? ' (' + m.error + ')' : '') };
14197
14301
  }
14198
- var then = observedText(m.value, m.words, m.http);
14302
+ var then = observedText(m.value, m.words, m.http, url);
14199
14303
  if ((typeof m.value === 'string') !== (typeof o.value === 'string')) {
14200
14304
  then = JSON.stringify(m.value);
14201
14305
  if (then === now) now = JSON.stringify(o.value);
@@ -14305,14 +14409,19 @@ var BOARD_HTML = `<!doctype html>
14305
14409
  s.observed.forEach(function (o) {
14306
14410
  var cell = el('td', { class: 'wrap' });
14307
14411
  var value;
14308
- if (o.value !== null && o.moved) {
14412
+ if (o.value !== null && o.moved && o.moved.status === 'none') {
14413
+ // Declared since the previous record: new, which is not a change.
14414
+ cell.appendChild(el('span', { class: 'chip added', text: S.observed.added }));
14415
+ value = observedText(o.value, o.words, o.http, declaredUrl(o));
14416
+ cell.appendChild(el('span', { text: value + (o.error ? ' (' + o.error + ')' : '') }));
14417
+ } else if (o.value !== null && o.moved) {
14309
14418
  var pair = movedText(o);
14310
14419
  cell.appendChild(el('span', { class: 'chip changed', text: S.observed.changed }));
14311
14420
  value = pair.now;
14312
14421
  cell.appendChild(el('span', { text: value + (o.error ? ' (' + o.error + ')' : '') }));
14313
14422
  cell.appendChild(el('div', { class: 'sub', text: pair.then }));
14314
14423
  } else {
14315
- if (o.value !== null) value = observedText(o.value, o.words, o.http);
14424
+ if (o.value !== null) value = observedText(o.value, o.words, o.http, declaredUrl(o));
14316
14425
  else if (o.previous && o.previous.status === 'value') value = fill(S.observed.missing, { value: shown(o.previous.value) });
14317
14426
  else if (o.previous && o.previous.status === 'unreadable') value = S.observed.missingPreviousUnreadable;
14318
14427
  else value = S.observed.missingNoPrevious;
@@ -14324,8 +14433,13 @@ var BOARD_HTML = `<!doctype html>
14324
14433
  rows.push(el('tr', null, [
14325
14434
  el('td', null, [el('div', { text: o.name }), what === null ? null : el('div', { class: 'sub', text: what })]),
14326
14435
  cell,
14327
- el('td', { text: o.observed_at }), el('td', { class: 'wrap', text: o.source })
14436
+ el('td', { text: observedAtText(o.observed_at), title: o.observed_at }),
14437
+ el('td', { class: 'wrap source', text: o.source })
14328
14438
  ]));
14439
+ // Its source again, drawn only on a narrow screen, where its column is not.
14440
+ rows.push(el('tr', { class: 'source' }, [el('td', { colspan: '4' }, [
14441
+ el('div', { class: 'sub', text: S.observed.source + ': ' + o.source })
14442
+ ])]));
14329
14443
  if (o.notes && o.notes.length > 0) {
14330
14444
  rows.push(el('tr', { class: 'notes' }, [el('td', { colspan: '4' }, [
14331
14445
  el('ul', { class: 'sub' }, o.notes.map(function (n) { return el('li', null, [prose(n, 'span')]); }))
@@ -14335,7 +14449,7 @@ var BOARD_HTML = `<!doctype html>
14335
14449
  observed = el('div', { class: 'scroll' }, [el('table', { class: 'grid' }, [
14336
14450
  el('tr', null, [
14337
14451
  el('th', { text: S.observed.name }), el('th', { text: S.observed.value }),
14338
- el('th', { text: S.observed.observedAt }), el('th', { text: S.observed.source })
14452
+ el('th', { text: S.observed.observedAt }), el('th', { class: 'source', text: S.observed.source })
14339
14453
  ])
14340
14454
  ].concat(rows))]);
14341
14455
  }
@@ -14666,7 +14780,22 @@ var BOARD_HTML = `<!doctype html>
14666
14780
  ['done', 'part', 'blocked', 'shelved', 'none', 'unverified'].forEach(function (s) {
14667
14781
  legend.appendChild(el('span', null, [el('span', { class: 'st ' + s }), stateName(s)]));
14668
14782
  });
14783
+ // The cells that moved since the previous record: their mark in the
14784
+ // legend, and each with the state it had, which a cell's title alone says
14785
+ // only to a pointer that hovers.
14786
+ var moved = [];
14787
+ x.lanes.forEach(function (lane) {
14788
+ lane.cells.forEach(function (c, i) {
14789
+ if (c.moved_from) moved.push({ lane: lane.name, stage: x.stages[i] ? x.stages[i].id : '', before: c.moved_from, after: c.state });
14790
+ });
14791
+ });
14792
+ if (moved.length > 0) legend.appendChild(el('span', null, [el('span', { class: 'moved-key' }), S.matrix.movedKey]));
14669
14793
  children.push(legend);
14794
+ if (moved.length > 0) {
14795
+ children.push(el('ul', { class: 'moved' }, moved.map(function (m) {
14796
+ return el('li', { text: fill(S.matrix.movedCell, { lane: m.lane, stage: m.stage, before: stateName(m.before), after: stateName(m.after) }) });
14797
+ })));
14798
+ }
14670
14799
  return section(S.sections.matrix, true, children);
14671
14800
  }
14672
14801
 
@@ -14715,16 +14844,18 @@ var BOARD_HTML = `<!doctype html>
14715
14844
  if (b.measures.length === 0) {
14716
14845
  return section(S.sections.measures, false, [el('p', { class: 'muted', text: S.measures.none })]);
14717
14846
  }
14847
+ // How much a number moved, right beside it: where the table is wider than
14848
+ // the screen, the lanes, which are words, are the column cut off.
14718
14849
  var head = el('tr', null, [
14719
14850
  el('th', { text: S.measures.id }), el('th', { text: S.measures.value }),
14720
- el('th', { text: S.measures.lanes }), el('th', { class: 'n', text: S.measures.delta })
14851
+ el('th', { class: 'n', text: S.measures.delta }), el('th', { text: S.measures.lanes })
14721
14852
  ]);
14722
14853
  var rows = b.measures.map(function (m) {
14723
14854
  return el('tr', { 'data-measure': m.id }, [
14724
14855
  el('td', null, [el('code', { text: m.id })]),
14725
14856
  el('td', { class: 'wrap', text: measureText(m) }),
14726
- el('td', { text: m.lanes.length === 0 ? '-' : m.lanes.join(', ') }),
14727
- el('td', { class: 'n', text: m.delta === null || m.delta === undefined ? '' : signed(m.delta) })
14857
+ el('td', { class: 'n', text: m.delta === null || m.delta === undefined ? '' : signed(m.delta) }),
14858
+ el('td', { class: 'lanes', text: m.lanes.length === 0 ? '-' : m.lanes.join(', ') })
14728
14859
  ]);
14729
14860
  });
14730
14861
  return section(S.sections.measures, false, [el('div', { class: 'scroll' }, [el('table', { class: 'grid' }, [head].concat(rows))])]);
@@ -14818,9 +14949,10 @@ var BOARD_HTML = `<!doctype html>
14818
14949
  // The board's own title, so that boards open in other tabs can be told apart.
14819
14950
  document.title = b.heading.title;
14820
14951
  // Where each lane stops, right under the summary's tiles, and the lanes,
14821
- // before the observations and the charts of the effort.
14822
- [heading(b), summary(b), matrix(b), lanes(b), observations(b), effort(b, false), measures(b), composition(b),
14823
- turns(b), recordRepos(b), recordComponents(b), footnotes(b)]
14952
+ // before the observations and the charts of the effort; then the
14953
+ // operator's turns, which are read most, before the tables of every value.
14954
+ [heading(b), summary(b), matrix(b), lanes(b), observations(b), effort(b, false), turns(b), measures(b),
14955
+ composition(b), recordRepos(b), recordComponents(b), footnotes(b)]
14824
14956
  .forEach(function (node) { root.appendChild(node); });
14825
14957
  }
14826
14958
 
@@ -14877,7 +15009,7 @@ var BOARD_HTML = `<!doctype html>
14877
15009
  el('td', { text: unmeasured('fetched_at', r.fetched_at, when) })
14878
15010
  ]);
14879
15011
  });
14880
- return el('div', { class: 'scroll' }, [el('table', { class: 'grid' }, [head].concat(rows))]);
15012
+ return el('div', { class: 'scroll' }, [el('table', { class: 'grid repos' }, [head].concat(rows))]);
14881
15013
  }
14882
15014
  function liveRepos(l) {
14883
15015
  return section(S.live.repos, false, [reposTable(l.repos, missingIn(l), false)]);
@@ -14974,9 +15106,12 @@ var BOARD_HTML = `<!doctype html>
14974
15106
  }));
14975
15107
  return section(S.live.components, false, [body]);
14976
15108
  }
14977
- function liveFootnotes(l, why) {
15109
+ // noRecord: whether to say how a record comes to be drawn, which is said
15110
+ // only where the workspace has no record.
15111
+ function liveFootnotes(l, why, noRecord) {
14978
15112
  var f = l.footnotes;
14979
- var items = [el('li', { text: S.live.note }), el('li', { text: S.live.judged })];
15113
+ var items = [el('li', { text: S.live.note })];
15114
+ if (noRecord) items.push(el('li', { text: S.live.judged }));
14980
15115
  if (why === 'no_board') items.push(el('li', { text: S.unavailable.noBoard }));
14981
15116
  if (f.not_found.length > 0) {
14982
15117
  items.push(el('li', null, [el('b', { text: S.footnotes.notMeasured }), el('ul', null, f.not_found.map(function (n) {
@@ -14997,19 +15132,27 @@ var BOARD_HTML = `<!doctype html>
14997
15132
  var drawn = ++liveDrawn;
14998
15133
  var nav = $('records');
14999
15134
  clear(nav);
15000
- // Asked for with ?live=1: lead back to the records, when there are any.
15001
- if (why === null) {
15002
- getJson(apiPath('board')).then(function (b) {
15003
- var list = b && b.page && b.page.records ? b.page.records : [];
15004
- if (drawn === liveDrawn && list.length > 0) nav.appendChild(el('a', { href: boardHref(''), text: S.live.toRecords }));
15005
- }).catch(function () {});
15006
- }
15007
15135
  var root = $('board');
15008
15136
  clear(root);
15009
15137
  setStatus('', false);
15010
15138
  var l = d.live;
15011
- [liveHeading(l, why), effort(l, true), liveRepos(l), liveTrail(l), liveComponents(l), liveFootnotes(l, why)]
15139
+ var foot = liveFootnotes(l, why, why !== null);
15140
+ [liveHeading(l, why), effort(l, true), liveRepos(l), liveTrail(l), liveComponents(l), foot]
15012
15141
  .forEach(function (node) { root.appendChild(node); });
15142
+ // Asked for with ?live=1: lead back to the records when there are any,
15143
+ // and say how one comes to be drawn when there are none.
15144
+ if (why === null) {
15145
+ getJson(apiPath('board')).then(function (b) {
15146
+ var list = b && b.page && b.page.records ? b.page.records : [];
15147
+ if (drawn !== liveDrawn) return;
15148
+ if (list.length > 0) {
15149
+ nav.appendChild(el('a', { href: boardHref(''), text: S.live.toRecords }));
15150
+ } else {
15151
+ root.removeChild(foot);
15152
+ root.appendChild(liveFootnotes(l, why, true));
15153
+ }
15154
+ }).catch(function () {});
15155
+ }
15013
15156
  }
15014
15157
 
15015
15158
  function getJson(url) {
@@ -15371,8 +15514,12 @@ var VIEW_HTML = `<!doctype html>
15371
15514
  if (!st.checked) return el('span', { class: 'badge', text: 'freshness unknown' });
15372
15515
  if (st.unverifiableSessions > 0)
15373
15516
  return el('span', { class: 'badge danger', text: '\u26A0 ' + st.unverifiableSessions + ' unverifiable \u2014 run verify' });
15374
- if (st.newSessions > 0)
15375
- return el('span', { class: 'badge danger', text: '\u26A0 ' + st.newSessions + ' uncaptured \u2014 run refresh' });
15517
+ // The updated ones are said beside the uncaptured ones, so the badge counts
15518
+ // what measure's freshness does ("1 new, 1 updated").
15519
+ if (st.newSessions > 0) {
15520
+ var updated = st.updatedSessions > 0 ? ', ' + st.updatedSessions + ' updated' : '';
15521
+ return el('span', { class: 'badge danger', text: '\u26A0 ' + st.newSessions + ' uncaptured' + updated + ' \u2014 run refresh' });
15522
+ }
15376
15523
  if (st.updatedSessions > 0)
15377
15524
  return el('span', { class: 'badge warn', text: st.updatedSessions + ' updated \u2014 run refresh' });
15378
15525
  return el('span', { class: 'badge ok', text: 'up to date' });
@@ -16595,7 +16742,7 @@ function installExperimentalNotice(program2, names = EXPERIMENTAL_COMMANDS, writ
16595
16742
  function readBuildStamp() {
16596
16743
  if (false) return void 0;
16597
16744
  try {
16598
- return JSON.parse('{"version":"0.74.0","commit":"31f16be","committedAt":"2026-10-23T12:42:48+09:00"}');
16745
+ return JSON.parse('{"version":"0.74.1","commit":"f08e08f","committedAt":"2026-10-23T19:05:22+09:00"}');
16599
16746
  } catch {
16600
16747
  return void 0;
16601
16748
  }