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