@basou/cli 0.71.1 → 0.72.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/dist/index.js CHANGED
@@ -842,11 +842,13 @@ import {
842
842
  assertBasouRootSafe as assertBasouRootSafe3,
843
843
  BOARD_INIT_SAVE,
844
844
  basouPaths as basouPaths4,
845
+ boardDeclarationSnapshot,
845
846
  boardGuide,
846
847
  boardInitText,
847
848
  buildRecord,
848
849
  byCodePoint,
849
850
  diffCells,
851
+ diffDeclarations,
850
852
  diffObserved,
851
853
  displayPath as displayPath3,
852
854
  effortStartOf,
@@ -1042,6 +1044,8 @@ import {
1042
1044
  CLAUDE_IMPORT_SOURCE,
1043
1045
  CODEX_IMPORT_SOURCE,
1044
1046
  classifyFilesBySourceRoot,
1047
+ claudeTranscriptOwnRecords,
1048
+ claudeTranscriptRecordUuid,
1045
1049
  claudeTranscriptToImportPayload,
1046
1050
  codexRolloutToImportPayload,
1047
1051
  displayPath as displayPath2,
@@ -1142,24 +1146,43 @@ async function doRunImportClaudeCode(options, ctx) {
1142
1146
  });
1143
1147
  const projectsRoot = ctx.claudeProjectsDir ?? join2(homedir(), ".claude", "projects");
1144
1148
  const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
1149
+ const store = await loadImportStore(paths, options, CLAUDE_IMPORT_SOURCE);
1150
+ const conversations = await indexConversations(
1151
+ await listTranscriptFiles(projectsRoot, projectPaths),
1152
+ ctx.transcriptCreatedAtMs ?? birthTimeMs,
1153
+ (externalId) => earliestStart(store.byExternalId.get(externalId) ?? [])
1154
+ );
1145
1155
  const projectSet = new Set(projectPaths);
1146
1156
  const candidates = files.map((file) => {
1147
1157
  const externalId = basename3(file, ".jsonl");
1158
+ const { root, older } = conversations.place(file);
1148
1159
  return {
1149
1160
  externalId,
1150
1161
  sourcePath: file,
1151
- toPayload: async () => {
1162
+ // An earlier import that took the copies too began the session at the
1163
+ // conversation's first record or before it; one without them begins after
1164
+ // the last copy.
1165
+ holdsCopies: (prior) => older.length > 0 && root?.timestamp !== void 0 && Date.parse(prior.startedAt) <= Date.parse(root.timestamp),
1166
+ toPayload: async (notBefore) => {
1152
1167
  const { records, sizeBytes } = await readJsonlRecords(file);
1153
1168
  const cwd = firstTranscriptCwd(records);
1154
- if (cwd === void 0 || !projectSet.has(cwd)) return null;
1169
+ if (cwd === void 0 || !projectSet.has(cwd))
1170
+ return { payload: null, leftOutCopies: false };
1155
1171
  const observation2 = await readSessionObservation(paths.observations, externalId);
1156
1172
  const observedFiles = observation2 === null ? [] : observedFilesOf(observation2);
1157
- return claudeTranscriptToImportPayload(records, {
1158
- workspaceId: manifest.workspace.id,
1159
- externalId,
1160
- sourceSizeBytes: sizeBytes,
1161
- ...observedFiles.length > 0 ? { observedFiles } : {}
1173
+ const own = claudeTranscriptOwnRecords(records, {
1174
+ copiedUuids: await conversations.uuidsOf(older),
1175
+ ...notBefore !== void 0 ? { notBefore } : {}
1162
1176
  });
1177
+ return {
1178
+ payload: claudeTranscriptToImportPayload(own, {
1179
+ workspaceId: manifest.workspace.id,
1180
+ externalId,
1181
+ sourceSizeBytes: sizeBytes,
1182
+ ...observedFiles.length > 0 ? { observedFiles } : {}
1183
+ }),
1184
+ leftOutCopies: own.length < records.length
1185
+ };
1163
1186
  }
1164
1187
  };
1165
1188
  });
@@ -1167,7 +1190,7 @@ async function doRunImportClaudeCode(options, ctx) {
1167
1190
  paths,
1168
1191
  manifest,
1169
1192
  options,
1170
- CLAUDE_IMPORT_SOURCE,
1193
+ store,
1171
1194
  candidates,
1172
1195
  projectPaths,
1173
1196
  hasDeclaredBoundary(options, manifest)
@@ -1189,18 +1212,21 @@ async function doRunImportCodex(options, ctx) {
1189
1212
  sourcePath: file,
1190
1213
  toPayload: async () => {
1191
1214
  const { records, sizeBytes } = await readJsonlRecords(file);
1192
- return codexRolloutToImportPayload(records, {
1193
- workspaceId: manifest.workspace.id,
1194
- externalId,
1195
- sourceSizeBytes: sizeBytes
1196
- });
1215
+ return {
1216
+ payload: codexRolloutToImportPayload(records, {
1217
+ workspaceId: manifest.workspace.id,
1218
+ externalId,
1219
+ sourceSizeBytes: sizeBytes
1220
+ }),
1221
+ leftOutCopies: false
1222
+ };
1197
1223
  }
1198
1224
  }));
1199
1225
  await importDerivedSessions(
1200
1226
  paths,
1201
1227
  manifest,
1202
1228
  options,
1203
- CODEX_IMPORT_SOURCE,
1229
+ await loadImportStore(paths, options, CODEX_IMPORT_SOURCE),
1204
1230
  candidates,
1205
1231
  projectPaths,
1206
1232
  hasDeclaredBoundary(options, manifest)
@@ -1225,14 +1251,8 @@ async function resolveImportTarget(ctx) {
1225
1251
  const manifest = await readManifest2(paths);
1226
1252
  return { repositoryRoot, paths, manifest };
1227
1253
  }
1228
- async function importDerivedSessions(paths, manifest, options, sourceKind, candidates, projectPaths, boundaryDeclared) {
1229
- await assertSessionStoreSafe(paths);
1230
- if (options.dryRun !== true) await assertLockStoreSafe2(paths);
1231
- const {
1232
- byExternalId: existingByExternalId,
1233
- notDirectories,
1234
- yamlNotFiles
1235
- } = await loadExistingByExternalId(paths, sourceKind);
1254
+ async function importDerivedSessions(paths, manifest, options, store, candidates, projectPaths, boundaryDeclared) {
1255
+ const { byExternalId: existingByExternalId, notDirectories, yamlNotFiles } = store;
1236
1256
  let notDirectoriesWarned = false;
1237
1257
  const seenThisRun = /* @__PURE__ */ new Set();
1238
1258
  const crossProjectCheck = boundaryDeclared;
@@ -1260,7 +1280,8 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
1260
1280
  skippedLegacy: 0,
1261
1281
  skippedDecreased: 0,
1262
1282
  skippedDuplicate: 0,
1263
- skippedUnverifiable: 0
1283
+ skippedUnverifiable: 0,
1284
+ droppedCopies: 0
1264
1285
  };
1265
1286
  let sanitizedPaths = 0;
1266
1287
  const validate = (payload) => {
@@ -1276,30 +1297,44 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
1276
1297
  }
1277
1298
  return parsed.data;
1278
1299
  };
1279
- for (const { externalId, sourcePath, toPayload } of candidates) {
1300
+ for (const { externalId, sourcePath, toPayload, holdsCopies } of candidates) {
1280
1301
  if (seenThisRun.has(externalId)) {
1281
1302
  counts.skippedExisting++;
1282
1303
  continue;
1283
1304
  }
1284
1305
  const priors = existingByExternalId.get(externalId) ?? [];
1285
1306
  if (priors.length > 0 && options.force !== true) {
1286
- const prior = await classifyReimport(priors, sourcePath, externalId, counts);
1287
- if (prior === null) continue;
1288
- const payload2 = validate(await toPayload());
1307
+ const decision = await classifyReimport(priors, sourcePath, externalId, counts, holdsCopies);
1308
+ if (decision === null) continue;
1309
+ const { prior, reason } = decision;
1310
+ const derived = await toPayload(prior.startedAt);
1311
+ const payload2 = validate(derived.payload);
1289
1312
  if (payload2 === null) {
1313
+ if (reason === "holds_copies") {
1314
+ console.error(
1315
+ `Import: ${externalId} added no action of its own after the records it copied from an older transcript; its session, imported with those records, is left as it is`
1316
+ );
1317
+ }
1290
1318
  counts.skippedNoAction++;
1291
1319
  continue;
1292
1320
  }
1321
+ if (reason === "holds_copies" && !derived.leftOutCopies) {
1322
+ counts.skippedExisting++;
1323
+ continue;
1324
+ }
1293
1325
  const readSize = payload2.session.source.source_size_bytes;
1294
- if (prior.sourceSizeBytes !== void 0 && readSize !== void 0 && readSize <= prior.sourceSizeBytes) {
1326
+ if (prior.sourceSizeBytes !== void 0 && readSize !== void 0 && (reason === "grew" ? readSize <= prior.sourceSizeBytes : readSize < prior.sourceSizeBytes)) {
1295
1327
  console.error(
1296
- `Import: ${externalId} source changed during read (now ${readSize} <= ${prior.sourceSizeBytes} bytes); re-import skipped`
1328
+ `Import: ${externalId} source changed during read (now ${readSize} bytes, was ${prior.sourceSizeBytes}); re-import skipped`
1297
1329
  );
1298
1330
  counts.skippedDecreased++;
1299
1331
  continue;
1300
1332
  }
1301
1333
  const outcome = await reimportPreservingId(paths, manifest, prior.sessionId, payload2, {
1302
- dryRun: options.dryRun === true
1334
+ dryRun: options.dryRun === true,
1335
+ // What an earlier import derived from the copies predates the session's
1336
+ // fresh start; anything later that is not reproduced still refuses.
1337
+ ...derived.leftOutCopies ? { dropDerivedBefore: payload2.session.started_at } : {}
1303
1338
  });
1304
1339
  if (outcome.status === "skipped") {
1305
1340
  const detail = outcome.reason === "prior_events_unreadable" ? "prior events.jsonl has unreadable lines" : outcome.reason === "prior_chain_broken" ? "prior events.jsonl failed hash-chain verification (run 'basou verify')" : outcome.reason === "prior_yaml_invalid" ? "prior session.yaml does not load as a whole document" : "source changed in a non-append way (derived events would be dropped)";
@@ -1308,11 +1343,12 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
1308
1343
  continue;
1309
1344
  }
1310
1345
  counts.reimported++;
1346
+ if (holdsCopies?.(prior) === true) counts.droppedCopies++;
1311
1347
  seenThisRun.add(externalId);
1312
1348
  await noteCrossProject(externalId, payload2);
1313
1349
  continue;
1314
1350
  }
1315
- const payload = validate(await toPayload());
1351
+ const payload = validate((await toPayload(earliestStart(priors))).payload);
1316
1352
  if (payload === null) {
1317
1353
  counts.skippedNoAction++;
1318
1354
  continue;
@@ -1363,7 +1399,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
1363
1399
  }
1364
1400
  printImportResult(options, results, counts);
1365
1401
  }
1366
- async function classifyReimport(priors, sourcePath, externalId, counts) {
1402
+ async function classifyReimport(priors, sourcePath, externalId, counts, holdsCopies) {
1367
1403
  if (priors.length > 1) {
1368
1404
  console.error(
1369
1405
  `Import: ${externalId} has ${priors.length} prior sessions; re-import skipped (use --force)`
@@ -1386,6 +1422,7 @@ async function classifyReimport(priors, sourcePath, externalId, counts) {
1386
1422
  return null;
1387
1423
  }
1388
1424
  if (currentSize === prior.sourceSizeBytes) {
1425
+ if (holdsCopies?.(prior) === true) return { prior, reason: "holds_copies" };
1389
1426
  counts.skippedExisting++;
1390
1427
  return null;
1391
1428
  }
@@ -1396,7 +1433,15 @@ async function classifyReimport(priors, sourcePath, externalId, counts) {
1396
1433
  counts.skippedDecreased++;
1397
1434
  return null;
1398
1435
  }
1399
- return prior;
1436
+ return { prior, reason: "grew" };
1437
+ }
1438
+ function earliestStart(priors) {
1439
+ let earliest;
1440
+ for (const { startedAt } of priors) {
1441
+ if (earliest === void 0 || Date.parse(startedAt) < Date.parse(earliest))
1442
+ earliest = startedAt;
1443
+ }
1444
+ return earliest;
1400
1445
  }
1401
1446
  function encodeProjectDir(projectPath) {
1402
1447
  return projectPath.replace(/[^a-zA-Z0-9]/g, "-");
@@ -1408,6 +1453,11 @@ function firstTranscriptCwd(records) {
1408
1453
  }
1409
1454
  return void 0;
1410
1455
  }
1456
+ async function loadImportStore(paths, options, sourceKind) {
1457
+ await assertSessionStoreSafe(paths);
1458
+ if (options.dryRun !== true) await assertLockStoreSafe2(paths);
1459
+ return loadExistingByExternalId(paths, sourceKind);
1460
+ }
1411
1461
  async function loadExistingByExternalId(paths, sourceKind) {
1412
1462
  const byExternalId = /* @__PURE__ */ new Map();
1413
1463
  const add = (externalId, prior) => {
@@ -1433,7 +1483,8 @@ async function loadExistingByExternalId(paths, sourceKind) {
1433
1483
  }
1434
1484
  if (session.session.source.kind !== sourceKind) continue;
1435
1485
  const sourceSizeBytes = session.session.source.source_size_bytes;
1436
- const prior = sourceSizeBytes !== void 0 ? { sessionId, sourceSizeBytes } : { sessionId };
1486
+ const startedAt = session.session.started_at;
1487
+ const prior = sourceSizeBytes !== void 0 ? { sessionId, startedAt, sourceSizeBytes } : { sessionId, startedAt };
1437
1488
  const ext = session.session.source.external_id;
1438
1489
  if (typeof ext === "string" && ext.length > 0) {
1439
1490
  add(ext, prior);
@@ -1478,6 +1529,153 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
1478
1529
  }
1479
1530
  return [...new Set(files)].sort();
1480
1531
  }
1532
+ async function listTranscriptFiles(projectsRoot, projectPaths) {
1533
+ const files = [];
1534
+ for (const projectPath of projectPaths) {
1535
+ const transcriptDir = join2(projectsRoot, encodeProjectDir(projectPath));
1536
+ let entries;
1537
+ try {
1538
+ entries = await readdir(transcriptDir);
1539
+ } catch (error) {
1540
+ if (findErrorCode2(error, "ENOENT")) continue;
1541
+ throw new Error("Failed to read Claude transcript directory", { cause: error });
1542
+ }
1543
+ for (const name of entries) {
1544
+ if (name.endsWith(".jsonl")) files.push(join2(transcriptDir, name));
1545
+ }
1546
+ }
1547
+ return [...new Set(files)].sort();
1548
+ }
1549
+ async function indexConversations(files, createdAtMs, importedStartedAt) {
1550
+ const uuidsByFile = /* @__PURE__ */ new Map();
1551
+ const uuidsIn = async (file) => {
1552
+ let held = uuidsByFile.get(file);
1553
+ if (held === void 0) {
1554
+ held = await readTranscriptUuids(file);
1555
+ uuidsByFile.set(file, held);
1556
+ }
1557
+ return held;
1558
+ };
1559
+ const rootOf = /* @__PURE__ */ new Map();
1560
+ const byRoot = /* @__PURE__ */ new Map();
1561
+ for (const file of files) {
1562
+ const root = await readConversationRoot(file);
1563
+ if (root === void 0) continue;
1564
+ rootOf.set(file, root);
1565
+ const entry = { file, externalId: basename3(file, ".jsonl"), createdAtMs: Number.NaN };
1566
+ const list = byRoot.get(root.uuid);
1567
+ if (list === void 0) byRoot.set(root.uuid, [entry]);
1568
+ else list.push(entry);
1569
+ }
1570
+ for (const list of byRoot.values()) {
1571
+ if (list.length < 2) continue;
1572
+ for (const entry of list) entry.createdAtMs = await createdAtMs(entry.file);
1573
+ list.sort((a, b) => {
1574
+ const knownA = Number.isFinite(a.createdAtMs) && a.createdAtMs > 0;
1575
+ const knownB = Number.isFinite(b.createdAtMs) && b.createdAtMs > 0;
1576
+ if (knownA && knownB && a.createdAtMs !== b.createdAtMs) return a.createdAtMs - b.createdAtMs;
1577
+ if (knownA !== knownB) return knownA ? -1 : 1;
1578
+ return a.externalId < b.externalId ? -1 : a.externalId > b.externalId ? 1 : 0;
1579
+ });
1580
+ const startedAtMs = /* @__PURE__ */ new Map();
1581
+ for (const entry of list) {
1582
+ const ms = Date.parse(importedStartedAt(entry.externalId) ?? "");
1583
+ if (!Number.isNaN(ms)) startedAtMs.set(entry, ms);
1584
+ }
1585
+ const imported = [...startedAtMs.keys()].sort(
1586
+ (a, b) => (startedAtMs.get(a) ?? 0) - (startedAtMs.get(b) ?? 0)
1587
+ );
1588
+ list.forEach((entry, index) => {
1589
+ if (startedAtMs.has(entry)) list[index] = imported.shift() ?? entry;
1590
+ });
1591
+ const rootMs = Date.parse(rootOf.get(list[0]?.file ?? "")?.timestamp ?? "");
1592
+ const forks = list.filter((entry) => (startedAtMs.get(entry) ?? Number.NaN) > rootMs);
1593
+ if (forks.length === 0) continue;
1594
+ const rank = new Map(list.map((entry, index) => [entry, index]));
1595
+ const countedBy = /* @__PURE__ */ new Map();
1596
+ for (const [index, entry] of list.entries()) {
1597
+ if (startedAtMs.has(entry)) continue;
1598
+ for (const fork of forks) {
1599
+ const forkRank = rank.get(fork) ?? index;
1600
+ if (forkRank < index) continue;
1601
+ let counted = countedBy.get(fork);
1602
+ if (counted === void 0) {
1603
+ counted = await readTranscriptUuids(fork.file, importedStartedAt(fork.externalId));
1604
+ countedBy.set(fork, counted);
1605
+ }
1606
+ if (counted.size === 0) continue;
1607
+ const held = await uuidsIn(entry.file);
1608
+ if ([...counted].some((uuid) => held.has(uuid))) {
1609
+ rank.set(entry, Math.max(rank.get(entry) ?? index, forkRank + 0.5));
1610
+ }
1611
+ }
1612
+ }
1613
+ list.sort((a, b) => (rank.get(a) ?? 0) - (rank.get(b) ?? 0));
1614
+ }
1615
+ return {
1616
+ place(file) {
1617
+ const root = rootOf.get(file);
1618
+ if (root === void 0) return { root: void 0, older: [] };
1619
+ const list = byRoot.get(root.uuid) ?? [];
1620
+ const at = list.findIndex((entry) => entry.file === file);
1621
+ return { root, older: list.slice(0, Math.max(at, 0)).map((entry) => entry.file) };
1622
+ },
1623
+ async uuidsOf(older) {
1624
+ const uuids = /* @__PURE__ */ new Set();
1625
+ for (const file of older) {
1626
+ for (const uuid of await uuidsIn(file)) uuids.add(uuid);
1627
+ }
1628
+ return uuids;
1629
+ }
1630
+ };
1631
+ }
1632
+ async function readTranscriptUuids(file, notBefore) {
1633
+ const uuids = /* @__PURE__ */ new Set();
1634
+ try {
1635
+ const { records } = await readJsonlRecords(file);
1636
+ const kept = notBefore === void 0 ? records : claudeTranscriptOwnRecords(records, { notBefore });
1637
+ for (const record of kept) {
1638
+ const uuid = claudeTranscriptRecordUuid(record);
1639
+ if (uuid !== void 0) uuids.add(uuid);
1640
+ }
1641
+ } catch {
1642
+ }
1643
+ return uuids;
1644
+ }
1645
+ async function readConversationRoot(file) {
1646
+ const stream = createReadStream(file, { encoding: "utf8" });
1647
+ const rl = createInterface({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
1648
+ try {
1649
+ for await (const line of rl) {
1650
+ const trimmed = line.trim();
1651
+ if (trimmed.length === 0) continue;
1652
+ let parsed;
1653
+ try {
1654
+ parsed = JSON.parse(trimmed);
1655
+ } catch {
1656
+ continue;
1657
+ }
1658
+ if (!isObject(parsed)) continue;
1659
+ const uuid = claudeTranscriptRecordUuid(parsed);
1660
+ if (uuid === void 0) continue;
1661
+ const timestamp = parsed.timestamp;
1662
+ return typeof timestamp === "string" && timestamp.length > 0 ? { uuid, timestamp } : { uuid };
1663
+ }
1664
+ return void 0;
1665
+ } catch {
1666
+ return void 0;
1667
+ } finally {
1668
+ rl.close();
1669
+ stream.destroy();
1670
+ }
1671
+ }
1672
+ async function birthTimeMs(file) {
1673
+ try {
1674
+ return (await stat2(file)).birthtimeMs;
1675
+ } catch {
1676
+ return Number.NaN;
1677
+ }
1678
+ }
1481
1679
  async function pathExists(file) {
1482
1680
  try {
1483
1681
  await stat2(file);
@@ -1613,7 +1811,8 @@ function printImportResult(options, results, counts) {
1613
1811
  skippedLegacy,
1614
1812
  skippedDecreased,
1615
1813
  skippedDuplicate,
1616
- skippedUnverifiable
1814
+ skippedUnverifiable,
1815
+ droppedCopies
1617
1816
  } = counts;
1618
1817
  if (options.json === true) {
1619
1818
  console.log(
@@ -1627,6 +1826,7 @@ function printImportResult(options, results, counts) {
1627
1826
  imported_count: results.length,
1628
1827
  replaced_count: replaced,
1629
1828
  reimported_count: reimported,
1829
+ dropped_copies_count: droppedCopies,
1630
1830
  skipped_no_action: skippedNoAction,
1631
1831
  skipped_already_imported: skippedExisting,
1632
1832
  skipped_legacy_untracked: skippedLegacy,
@@ -1649,10 +1849,11 @@ function printImportResult(options, results, counts) {
1649
1849
  skipParts.push(`${skippedUnverifiable} unverifiable (run 'basou verify')`);
1650
1850
  const skipSuffix = skipParts.length > 0 ? `; skipped ${skipParts.join(", ")}` : "";
1651
1851
  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)` : "";
1652
1853
  if (isDry) {
1653
1854
  const parts = [];
1654
1855
  if (results.length > 0) parts.push(`import ${results.length} session(s) (${eventsPart})`);
1655
- if (reimported > 0) parts.push(`re-import ${reimported} changed session(s)`);
1856
+ if (reimported > 0) parts.push(`re-import ${reimported} changed session(s)${copiesPart}`);
1656
1857
  const head = parts.length > 0 ? `Dry run: would ${parts.join(", ")}` : "Dry run: no changes";
1657
1858
  console.log(`${head}${skipSuffix}`);
1658
1859
  return;
@@ -1670,7 +1871,7 @@ function printImportResult(options, results, counts) {
1670
1871
  }
1671
1872
  if (reimported > 0) {
1672
1873
  segments.push(
1673
- `${results.length > 0 ? "re-imported" : "Re-imported"} ${reimported} changed session(s)`
1874
+ `${results.length > 0 ? "re-imported" : "Re-imported"} ${reimported} changed session(s)${copiesPart}`
1674
1875
  );
1675
1876
  }
1676
1877
  console.log(`${segments.join(", ")}${skipSuffix}`);
@@ -1756,6 +1957,7 @@ async function runImport(adapter, fn) {
1756
1957
  importedCount: readCount(json.imported_count),
1757
1958
  replacedCount: readCount(json.replaced_count),
1758
1959
  reimportedCount: readCount(json.reimported_count),
1960
+ droppedCopiesCount: readCount(json.dropped_copies_count),
1759
1961
  skippedNoAction: readCount(json.skipped_no_action),
1760
1962
  skippedAlreadyImported: readCount(json.skipped_already_imported),
1761
1963
  skippedLegacyUntracked: readCount(json.skipped_legacy_untracked),
@@ -2145,9 +2347,10 @@ async function doRunBoardMeasure(options, ctx) {
2145
2347
  throw new Error("--model must name a model.");
2146
2348
  }
2147
2349
  const loaded = await loadBoard(options, ctx, "board measure");
2148
- const measurement = await measureLoaded(loaded, ctx, options.model, await previousOf(loaded));
2350
+ const previous = await previousOf(loaded);
2351
+ const measurement = await measureLoaded(loaded, ctx, options.model, previous);
2149
2352
  if (options.json === true) console.log(JSON.stringify(measurement, null, 2));
2150
- else printMeasurementText(measurement, hasRecords(loaded));
2353
+ else printMeasurementText(measurement, hasRecords(loaded), declarationChanges(loaded, previous));
2151
2354
  if (!measurement.complete) process.exitCode = 1;
2152
2355
  return measurement;
2153
2356
  }
@@ -2162,16 +2365,17 @@ async function runBoardRecord(options, ctx = {}) {
2162
2365
  var RecordRefusal = class extends Error {
2163
2366
  };
2164
2367
  async function doRunBoardRecord(options, ctx) {
2165
- let result;
2368
+ let checked;
2166
2369
  try {
2167
- result = await checkAndRecord(options, ctx);
2370
+ checked = await checkAndRecord(options, ctx);
2168
2371
  } catch (error) {
2169
2372
  if (error instanceof RecordRefusal || !(error instanceof Error)) throw error;
2170
2373
  throw new RecordRefusal(`${error.message}
2171
2374
  Nothing was written.`, { cause: error.cause });
2172
2375
  }
2376
+ const { result, declared } = checked;
2173
2377
  if (options.json === true) console.log(JSON.stringify(result, null, 2));
2174
- else printRecordText(result);
2378
+ else printRecordText(result, declared ?? null);
2175
2379
  return result;
2176
2380
  }
2177
2381
  async function checkAndRecord(options, ctx) {
@@ -2197,9 +2401,9 @@ async function checkAndRecord(options, ctx) {
2197
2401
  ${parsed.errors.map((e) => ` - ${displayPath3(e)}`).join("\n")}`
2198
2402
  );
2199
2403
  }
2200
- const recordedAt = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
2201
2404
  const previous = await previousOf(loaded);
2202
2405
  const measurement = await measureLoaded(loaded, ctx, parsed.input.judged_by.model, previous);
2406
+ const recordedAt = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
2203
2407
  if (measurement.digest !== parsed.input.measure_digest) {
2204
2408
  throw new RecordRefusal(
2205
2409
  `measure_digest is ${parsed.input.measure_digest}, but the board measures ${measurement.digest} now: something it measures or board.yaml changed since the judgement, or basou was rebuilt or upgraded since that measurement. Measure again and judge that; nothing was written.`
@@ -2212,6 +2416,7 @@ ${parsed.errors.map((e) => ` - ${displayPath3(e)}`).join("\n")}`
2212
2416
  recordedAt,
2213
2417
  recordedWith: { basou: BASOU_CLI_VERSION, build: BASOU_BUILD?.commit ?? null }
2214
2418
  });
2419
+ const declared = options.json === true ? void 0 : declarationChanges(loaded, previous);
2215
2420
  let written = null;
2216
2421
  if (options.dryRun !== true) {
2217
2422
  let name;
@@ -2238,14 +2443,22 @@ ${parsed.errors.map((e) => ` - ${displayPath3(e)}`).join("\n")}`
2238
2443
  };
2239
2444
  }
2240
2445
  return {
2241
- record: written,
2242
- dry_run: options.dryRun === true,
2243
- complete: measurement.complete,
2244
- not_found: measurement.not_found,
2245
- order_anomalies: record.order_anomalies,
2246
- diff
2446
+ result: {
2447
+ record: written,
2448
+ dry_run: options.dryRun === true,
2449
+ complete: measurement.complete,
2450
+ not_found: measurement.not_found,
2451
+ order_anomalies: record.order_anomalies,
2452
+ diff
2453
+ },
2454
+ ...declared === void 0 ? {} : { declared }
2247
2455
  };
2248
2456
  }
2457
+ function declarationChanges(loaded, previous) {
2458
+ const { last } = previous;
2459
+ if (last.status !== "found") return null;
2460
+ return diffDeclarations(last.record.declaration, boardDeclarationSnapshot(loaded.declaration));
2461
+ }
2249
2462
  var INPUT_KEYS = /* @__PURE__ */ new Set([
2250
2463
  "measure_digest",
2251
2464
  "observed",
@@ -2395,7 +2608,7 @@ async function checkRecordsPlace(loaded, recordsDir, shown, notPrivate) {
2395
2608
  }
2396
2609
  }
2397
2610
  }
2398
- function printRecordText(result) {
2611
+ function printRecordText(result, declared) {
2399
2612
  const lines = [
2400
2613
  result.record === null ? "Dry run: the record checked out (nothing was written)." : `Recorded ${displayPath3(result.record)}`
2401
2614
  ];
@@ -2416,7 +2629,11 @@ function printRecordText(result) {
2416
2629
  ` observed ${displayPath3(o.name)}: ${observation(o.before)} -> ${observation(o.after)}`
2417
2630
  );
2418
2631
  }
2419
- lines.push(...diffLines(result.diff.measure, cells.length + observed.length > 0));
2632
+ const declaration = declarationLines(declared);
2633
+ lines.push(
2634
+ ...diffLines(result.diff.measure, cells.length + observed.length + declaration.length > 0),
2635
+ ...declaration
2636
+ );
2420
2637
  }
2421
2638
  if (!result.complete) {
2422
2639
  lines.push(`The measurement recorded is not complete (${result.not_found.length}):`);
@@ -2602,6 +2819,16 @@ function diffLines(diff, more) {
2602
2819
  if (lines.length === 0 && !more) lines.push(" nothing moved");
2603
2820
  return lines;
2604
2821
  }
2822
+ function declarationLines(changes) {
2823
+ if (changes === null) {
2824
+ return [
2825
+ " declaration: not compared (the previous record holds no declaration basou can compare with)"
2826
+ ];
2827
+ }
2828
+ return changes.map(
2829
+ (c) => ` declaration ${displayPath3(c.at)}: ${c.change === "reordered" ? "the order changed" : c.change}`
2830
+ );
2831
+ }
2605
2832
  function shownValue(value) {
2606
2833
  if (value === null) return "not measured";
2607
2834
  if (typeof value === "number") return String(Math.round(value * 1e4) / 1e4);
@@ -2733,12 +2960,18 @@ function axisLines(m) {
2733
2960
  for (const u of a.unjudged) lines.push(` (${u.trigger}) not judged: ${u.why}`);
2734
2961
  return lines;
2735
2962
  }
2736
- function printMeasurementText(m, hasRecords2) {
2963
+ function printMeasurementText(m, hasRecords2, declared) {
2737
2964
  const lines = [displayPath3(m.title)];
2738
2965
  const build = m.measured_with.build === null ? "" : ` (build ${m.measured_with.build})`;
2739
2966
  lines.push(`Measured ${m.measured_at} with basou ${m.measured_with.basou}${build}`);
2740
2967
  if (m.diff !== null) {
2741
- lines.push("", `Since the previous record ${m.diff.against}:`, ...diffLines(m.diff, false));
2968
+ const declaration = declarationLines(declared);
2969
+ lines.push(
2970
+ "",
2971
+ `Since the previous record ${m.diff.against}:`,
2972
+ ...diffLines(m.diff, declaration.length > 0),
2973
+ ...declaration
2974
+ );
2742
2975
  } else if (m.not_found.some((n) => n.at === "diff")) {
2743
2976
  lines.push("", "The previous record could not be read (see Not measured).");
2744
2977
  } else {
@@ -2753,7 +2986,8 @@ function printMeasurementText(m, hasRecords2) {
2753
2986
  const width = Math.max(...measures.map(([id]) => id.length));
2754
2987
  lines.push("", "Measures:");
2755
2988
  for (const [id, measure] of measures) {
2756
- const unit = measure.value === null ? "" : ` ${displayPath3(measure.unit)}`;
2989
+ const counted = typeof measure.value === "number" || Array.isArray(measure.value);
2990
+ const unit = counted ? ` ${displayPath3(measure.unit)}` : "";
2757
2991
  const listed = Array.isArray(measure.value) ? " (paths in --json)" : "";
2758
2992
  const lane = measure.lane === void 0 ? "" : ` [${measure.lane.join(", ")}]`;
2759
2993
  lines.push(` ${id.padEnd(width)} ${shownValue(measure.value)}${unit}${listed}${lane}`);
@@ -9996,7 +10230,10 @@ function describeImport(outcome) {
9996
10230
  }
9997
10231
  const verb = outcome.dryRun ? "would import" : "imported";
9998
10232
  const parts = [`${outcome.importedCount} session(s)`, `${outcome.eventTotal} events`];
9999
- if (outcome.reimportedCount > 0) parts.push(`${outcome.reimportedCount} re-imported`);
10233
+ 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}`);
10236
+ }
10000
10237
  if (outcome.replacedCount > 0) parts.push(`${outcome.replacedCount} replaced`);
10001
10238
  if (outcome.skippedAlreadyImported > 0)
10002
10239
  parts.push(`${outcome.skippedAlreadyImported} already imported`);
@@ -13754,6 +13991,11 @@ var BOARD_HTML = `<!doctype html>
13754
13991
  .chip.blocked { background: var(--chip-blocked); }
13755
13992
  .chip.unverified { background: var(--chip-unverified); }
13756
13993
  .muted { color: var(--fg-2); }
13994
+ .sub { font-size: 12px; color: var(--fg-2); overflow-wrap: anywhere; }
13995
+ ul.sub { margin: 0; padding-left: 18px; }
13996
+ td.wrap { overflow-wrap: anywhere; min-width: 10em; }
13997
+ tr.notes td { padding-top: 0; }
13998
+ .chip.changed { margin-left: 0; margin-right: 6px; background: var(--chip-unverified); }
13757
13999
  .bar { height: 10px; background: var(--fill); border-radius: 5px; min-width: 120px; position: relative; overflow: hidden; }
13758
14000
  .bar span { position: absolute; left: 0; top: 0; bottom: 0; background: var(--s1); }
13759
14001
  figure { margin: 16px 0; }
@@ -13883,6 +14125,60 @@ var BOARD_HTML = `<!doctype html>
13883
14125
  return JSON.stringify(k) + ':' + v;
13884
14126
  }).join(',') + '}';
13885
14127
  }
14128
+ // The parts of a value drawn on one line, and what an http value says of each word.
14129
+ var DOT = ' ' + String.fromCharCode(183) + ' ';
14130
+ var FOUND = String.fromCharCode(10003);
14131
+ var NOT_FOUND = String.fromCharCode(10007);
14132
+ // An http value read as its parts: the status, the redirects, where it
14133
+ // landed, and what it read off the body (when the status is 2xx) or that it
14134
+ // did not. Numbers and strings are drawn as they are.
14135
+ function httpText(h) {
14136
+ var parts = [String(h.status), fill(S.observed.redirects, { n: h.redirects }), h.final_url];
14137
+ if (h.body === null) parts.push(S.observed.bodyNotRead);
14138
+ else if (h.body) {
14139
+ if (h.body.words) {
14140
+ parts.push(h.body.words.map(function (w) { return w.word + ' ' + (w.found ? FOUND : NOT_FOUND); }).join(' '));
14141
+ }
14142
+ if (h.body.count !== undefined) parts.push(fill(S.observed.count, { n: h.body.count }));
14143
+ if (h.body.capture !== undefined) {
14144
+ parts.push(h.body.capture === null ? S.observed.captureNone : fill(S.observed.capture, { value: JSON.stringify(h.body.capture) }));
14145
+ }
14146
+ }
14147
+ return parts.join(DOT);
14148
+ }
14149
+ // A value of an observation as the page draws it: an http value in its
14150
+ // shape read as its parts, one with words beside what it says of each, and
14151
+ // any other as it is (a string) or as JSON. A test lifts this function.
14152
+ function observedText(value, words, http) {
14153
+ if (http) return httpText(http);
14154
+ if (words) return withWords(value, words);
14155
+ return shown(value);
14156
+ }
14157
+ // The value of an observation that moved, and what the previous record
14158
+ // held: a value whose type alone moved ("12" and 12) is drawn as JSON, so
14159
+ // that the two do not read the same. A test lifts this function.
14160
+ function movedText(o) {
14161
+ var m = o.moved;
14162
+ var now = observedText(o.value, o.words, o.http);
14163
+ if (m.status === 'none') return { now: now, then: S.observed.beforeNone };
14164
+ if (m.value === null) {
14165
+ return { now: now, then: S.observed.beforeNotObserved + (m.error ? ' (' + m.error + ')' : '') };
14166
+ }
14167
+ var then = observedText(m.value, m.words, m.http);
14168
+ if ((typeof m.value === 'string') !== (typeof o.value === 'string')) {
14169
+ then = JSON.stringify(m.value);
14170
+ if (then === now) now = JSON.stringify(o.value);
14171
+ }
14172
+ return { now: now, then: fill(S.observed.before, { value: then }) + (m.error ? ' (' + m.error + ')' : '') };
14173
+ }
14174
+ // A measure's value: the paths of a file_paths by how many, a number with
14175
+ // its unit, and a string (a file's name, a capture), which is no amount,
14176
+ // without it.
14177
+ function measureText(m) {
14178
+ if (Array.isArray(m.value)) return num(m.value.length) + ' ' + m.unit;
14179
+ return num(m.value) + (typeof m.value === 'number' ? ' ' + m.unit : '');
14180
+ }
14181
+ function signed(n) { return (n > 0 ? '+' : '') + num(n); }
13886
14182
  function stateName(state) { return S.states[state] || state; }
13887
14183
  function setStatus(message, isErr) {
13888
14184
  var s = $('status');
@@ -13967,17 +14263,36 @@ var BOARD_HTML = `<!doctype html>
13967
14263
  if (s.observed.length === 0) {
13968
14264
  observed = el('p', { class: 'muted', text: S.observed.none });
13969
14265
  } else {
13970
- var rows = s.observed.map(function (o) {
14266
+ var rows = [];
14267
+ s.observed.forEach(function (o) {
14268
+ var cell = el('td', { class: 'wrap' });
13971
14269
  var value;
13972
- if (o.value !== null) value = o.words ? withWords(o.value, o.words) : shown(o.value);
13973
- else if (o.previous && o.previous.status === 'value') value = fill(S.observed.missing, { value: shown(o.previous.value) });
13974
- else if (o.previous && o.previous.status === 'unreadable') value = S.observed.missingPreviousUnreadable;
13975
- else value = S.observed.missingNoPrevious;
13976
- if (o.error) value += ' (' + o.error + ')';
13977
- return el('tr', null, [
13978
- el('td', { text: o.name }), el('td', { text: value }),
13979
- el('td', { text: o.observed_at }), el('td', { text: o.source })
13980
- ]);
14270
+ if (o.value !== null && o.moved) {
14271
+ var pair = movedText(o);
14272
+ cell.appendChild(el('span', { class: 'chip changed', text: S.observed.changed }));
14273
+ value = pair.now;
14274
+ cell.appendChild(el('span', { text: value + (o.error ? ' (' + o.error + ')' : '') }));
14275
+ cell.appendChild(el('div', { class: 'sub', text: pair.then }));
14276
+ } else {
14277
+ if (o.value !== null) value = observedText(o.value, o.words, o.http);
14278
+ else if (o.previous && o.previous.status === 'value') value = fill(S.observed.missing, { value: shown(o.previous.value) });
14279
+ else if (o.previous && o.previous.status === 'unreadable') value = S.observed.missingPreviousUnreadable;
14280
+ else value = S.observed.missingNoPrevious;
14281
+ if (o.error) value += ' (' + o.error + ')';
14282
+ cell.appendChild(el('span', { text: value }));
14283
+ }
14284
+ // What the record's declaration says it is and looks at, under its name.
14285
+ var what = o.kind ? [o.kind].concat(o.looks_at || []).join(DOT) : null;
14286
+ rows.push(el('tr', null, [
14287
+ el('td', null, [el('div', { text: o.name }), what === null ? null : el('div', { class: 'sub', text: what })]),
14288
+ cell,
14289
+ el('td', { text: o.observed_at }), el('td', { class: 'wrap', text: o.source })
14290
+ ]));
14291
+ if (o.notes && o.notes.length > 0) {
14292
+ rows.push(el('tr', { class: 'notes' }, [el('td', { colspan: '4' }, [
14293
+ el('ul', { class: 'sub' }, o.notes.map(function (n) { return el('li', null, [prose(n, 'span')]); }))
14294
+ ])]));
14295
+ }
13981
14296
  });
13982
14297
  observed = el('div', { class: 'scroll' }, [el('table', { class: 'grid' }, [
13983
14298
  el('tr', null, [
@@ -13987,12 +14302,16 @@ var BOARD_HTML = `<!doctype html>
13987
14302
  ].concat(rows))]);
13988
14303
  }
13989
14304
  var observedHeading = el('h3', { text: S.observed.heading }, [reported()]);
13990
- return section(S.sections.summary, false, [
14305
+ var children = [
13991
14306
  el('div', null, [prose(s.text), reported()]),
13992
14307
  tiles,
13993
14308
  observedHeading,
13994
14309
  observed
13995
- ]);
14310
+ ];
14311
+ if (s.previous_record === 'unreadable' && s.observed.length > 0) {
14312
+ children.push(el('p', { class: 'muted', text: S.observed.previousUnreadable }));
14313
+ }
14314
+ return section(S.sections.summary, false, children);
13996
14315
  }
13997
14316
 
13998
14317
  var SVGNS = 'http://www.w3.org/2000/svg';
@@ -14323,13 +14642,35 @@ var BOARD_HTML = `<!doctype html>
14323
14642
  el('ul', null, m.value.map(function (p) { return el('li', { text: p }); }))
14324
14643
  ])]);
14325
14644
  }
14326
- return el('li', { text: m.id + ': ' + num(m.value) + (m.value === null ? '' : ' ' + m.unit) });
14645
+ // A string (a file's name, a capture) is no amount: it takes no unit.
14646
+ return el('li', { text: m.id + ': ' + num(m.value) + (typeof m.value === 'number' ? ' ' + m.unit : '') });
14327
14647
  })));
14328
14648
  }
14329
14649
  return el('div', { class: 'lane', 'data-lane': lane.id }, body);
14330
14650
  }));
14331
14651
  }
14332
14652
 
14653
+ // Every measure of the record, with the lanes it is drawn in and how much a
14654
+ // number moved since the previous record.
14655
+ function measures(b) {
14656
+ if (b.measures.length === 0) {
14657
+ return section(S.sections.measures, false, [el('p', { class: 'muted', text: S.measures.none })]);
14658
+ }
14659
+ var head = el('tr', null, [
14660
+ el('th', { text: S.measures.id }), el('th', { text: S.measures.value }),
14661
+ el('th', { text: S.measures.lanes }), el('th', { class: 'n', text: S.measures.delta })
14662
+ ]);
14663
+ var rows = b.measures.map(function (m) {
14664
+ return el('tr', { 'data-measure': m.id }, [
14665
+ el('td', null, [el('code', { text: m.id })]),
14666
+ el('td', { class: 'wrap', text: measureText(m) }),
14667
+ el('td', { text: m.lanes.length === 0 ? '-' : m.lanes.join(', ') }),
14668
+ el('td', { class: 'n', text: m.delta === null || m.delta === undefined ? '' : signed(m.delta) })
14669
+ ]);
14670
+ });
14671
+ return section(S.sections.measures, false, [el('div', { class: 'scroll' }, [el('table', { class: 'grid' }, [head].concat(rows))])]);
14672
+ }
14673
+
14333
14674
  function composition(b) {
14334
14675
  if (b.composition.length === 0) {
14335
14676
  return section(S.sections.composition, false, [el('p', { class: 'muted', text: S.composition.none })]);
@@ -14417,7 +14758,8 @@ var BOARD_HTML = `<!doctype html>
14417
14758
  var b = page.board;
14418
14759
  // The board's own title, so that boards open in other tabs can be told apart.
14419
14760
  document.title = b.heading.title;
14420
- [heading(b), summary(b), effort(b, false), matrix(b), lanes(b), composition(b), turns(b), footnotes(b)]
14761
+ [heading(b), summary(b), effort(b, false), matrix(b), lanes(b), measures(b), composition(b), turns(b),
14762
+ recordRepos(b), recordComponents(b), footnotes(b)]
14421
14763
  .forEach(function (node) { root.appendChild(node); });
14422
14764
  }
14423
14765
 
@@ -14443,12 +14785,14 @@ var BOARD_HTML = `<!doctype html>
14443
14785
  function missingIn(l) {
14444
14786
  return function (at) { return l.footnotes.not_found.some(function (n) { return n.at === at; }); };
14445
14787
  }
14446
- function liveRepos(l) {
14447
- var missing = missingIn(l);
14788
+ // The repositories as a table. older: a record of version 1 or 2, which
14789
+ // measured no base, ahead or fetch time (drawn as '-') and held the
14790
+ // commits behind as behind_main.
14791
+ function reposTable(list, missing, older) {
14448
14792
  var labels = [S.live.repo, S.live.branch, S.live.head, S.live.lastCommit, S.live.commits, S.live.uncommitted, S.live.base, S.live.behind, S.live.ahead, S.live.fetchedAt];
14449
14793
  var numeric = [4, 5, 7, 8];
14450
14794
  var head = el('tr', null, labels.map(function (label, i) { return el('th', { class: numeric.indexOf(i) !== -1 ? 'n' : '', text: label }); }));
14451
- var rows = l.repos.map(function (r) {
14795
+ var rows = list.map(function (r) {
14452
14796
  var at = 'repos[' + r.path + ']';
14453
14797
  // A null the measurement says it could not measure, or one that means
14454
14798
  // so (a detached HEAD, no commit yet, no base, no fetch of it on
@@ -14457,6 +14801,7 @@ var BOARD_HTML = `<!doctype html>
14457
14801
  if (v !== null) return show(v);
14458
14802
  return missing(at) || missing(at + '.' + field) ? S.tiles.notMeasured : '-';
14459
14803
  };
14804
+ var unmeasured = function (field, v, show) { return older ? '-' : value(field, v, show); };
14460
14805
  return el('tr', null, [
14461
14806
  el('td', { text: r.name === null ? r.path : r.name }),
14462
14807
  el('td', { text: value('branch', r.branch, String) }),
@@ -14464,13 +14809,59 @@ var BOARD_HTML = `<!doctype html>
14464
14809
  el('td', { text: value('last_commit', r.last_commit, when) }),
14465
14810
  el('td', { class: 'n', text: num(r.commits) }),
14466
14811
  el('td', { class: 'n', text: num(r.uncommitted) }),
14467
- el('td', { text: value('base', r.base, String) }),
14468
- el('td', { class: 'n', text: value('behind', r.behind, num) }),
14469
- el('td', { class: 'n', text: value('ahead', r.ahead, num) }),
14470
- el('td', { text: value('fetched_at', r.fetched_at, when) })
14812
+ el('td', { text: unmeasured('base', r.base, String) }),
14813
+ el('td', { class: 'n', text: value(older ? 'behind_main' : 'behind', r.behind, num) }),
14814
+ el('td', { class: 'n', text: unmeasured('ahead', r.ahead, num) }),
14815
+ el('td', { text: unmeasured('fetched_at', r.fetched_at, when) })
14816
+ ]);
14817
+ });
14818
+ return el('div', { class: 'scroll' }, [el('table', { class: 'grid' }, [head].concat(rows))]);
14819
+ }
14820
+ function liveRepos(l) {
14821
+ return section(S.live.repos, false, [reposTable(l.repos, missingIn(l), false)]);
14822
+ }
14823
+ // The repositories as the record measured them, by its version.
14824
+ function recordRepos(b) {
14825
+ var r = b.repos;
14826
+ if (!r) return section(S.live.repos, false, [el('p', { class: 'muted', text: S.record.reposUnread })]);
14827
+ var children = [reposTable(r.list, missingIn(b), !r.base_measured)];
14828
+ if (!r.base_measured) children.push(el('p', { class: 'muted', text: fill(S.record.notMeasuredByVersion, { version: r.record_version }) }));
14829
+ return section(S.live.repos, false, children);
14830
+ }
14831
+ // The components the record found, by the lanes its registry counts them
14832
+ // in, then those in no lane, those not registered, and those gone.
14833
+ function recordComponents(b) {
14834
+ var c = b.components;
14835
+ if (!c) return section(S.live.components, false, [el('p', { class: 'muted', text: S.record.componentsUnread })]);
14836
+ if (!c.measured) {
14837
+ return section(S.live.components, false, [
14838
+ el('p', { class: 'muted', text: S.record.componentsNotMeasured }),
14839
+ el('ul', null, c.reasons.map(function (reason) { return el('li', { text: reason }); }))
14471
14840
  ]);
14841
+ }
14842
+ var item = function (x) {
14843
+ var kinds = x.kinds === null ? S.record.notFound : x.kinds.join(', ');
14844
+ var parts = [el('code', { text: x.key }), el('span', { class: 'muted', text: ' ' + kinds })];
14845
+ if (x.kind_changed) {
14846
+ parts.push(el('span', { class: 'chip changed', text: fill(S.record.kindChanged, { before: x.kind_changed.before.join(', ') }) }));
14847
+ }
14848
+ if (x.note) parts.push(el('div', { class: 'sub', text: x.note }));
14849
+ return el('li', { 'data-component': x.key }, parts);
14850
+ };
14851
+ var group = function (title, list, empty) {
14852
+ return [el('h3', { text: title }), list.length === 0 ? el('p', { class: 'muted', text: empty }) : el('ul', null, list.map(item))];
14853
+ };
14854
+ var children = [];
14855
+ c.lanes.forEach(function (lane) {
14856
+ children = children.concat(group(lane.name, lane.components, S.record.noComponentInLane));
14472
14857
  });
14473
- return section(S.live.repos, false, [el('div', { class: 'scroll' }, [el('table', { class: 'grid' }, [head].concat(rows))])]);
14858
+ children = children.concat(group(S.record.uncounted, c.uncounted, S.record.none));
14859
+ children = children.concat(group(S.record.unacknowledged, c.unacknowledged, S.record.none));
14860
+ children.push(el('h3', { text: S.record.gone }));
14861
+ children.push(c.gone === null ? el('p', { class: 'muted', text: S.record.goneUnknown })
14862
+ : c.gone.length === 0 ? el('p', { class: 'muted', text: S.record.none })
14863
+ : el('ul', null, c.gone.map(function (key) { return el('li', null, [el('code', { text: key })]); })));
14864
+ return section(S.live.components, false, children);
14474
14865
  }
14475
14866
  function liveTrail(l) {
14476
14867
  var t = l.trail;
@@ -16131,7 +16522,7 @@ function installExperimentalNotice(program2, names = EXPERIMENTAL_COMMANDS, writ
16131
16522
  function readBuildStamp() {
16132
16523
  if (false) return void 0;
16133
16524
  try {
16134
- return JSON.parse('{"version":"0.71.1","commit":"5097e32","committedAt":"2026-10-19T07:43:16+09:00"}');
16525
+ return JSON.parse('{"version":"0.72.0","commit":"f92df50","committedAt":"2026-10-20T12:15:21+09:00"}');
16135
16526
  } catch {
16136
16527
  return void 0;
16137
16528
  }