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