@basou/cli 0.74.0 → 0.75.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 +260 -52
- package/dist/index.js.map +1 -1
- package/dist/program.js +260 -52
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/program.js
CHANGED
|
@@ -861,6 +861,7 @@ import {
|
|
|
861
861
|
readPreviousRecords,
|
|
862
862
|
recordIds,
|
|
863
863
|
resolveViewLanguage,
|
|
864
|
+
setEntryOf,
|
|
864
865
|
shellWord,
|
|
865
866
|
writeRecord
|
|
866
867
|
} from "@basou/core";
|
|
@@ -1080,6 +1081,18 @@ var SHORT_ID_LEN2 = 6;
|
|
|
1080
1081
|
function collectPath(value, previous) {
|
|
1081
1082
|
return [...previous, value];
|
|
1082
1083
|
}
|
|
1084
|
+
function reimportFallText(change) {
|
|
1085
|
+
const parts = [];
|
|
1086
|
+
if (change.decisions < 0) {
|
|
1087
|
+
const n = -change.decisions;
|
|
1088
|
+
parts.push(`${n.toLocaleString("en-US")} fewer decision${n === 1 ? "" : "s"}`);
|
|
1089
|
+
}
|
|
1090
|
+
if (change.outputTokens < 0) {
|
|
1091
|
+
const n = -change.outputTokens;
|
|
1092
|
+
parts.push(`${n.toLocaleString("en-US")} fewer output token${n === 1 ? "" : "s"}`);
|
|
1093
|
+
}
|
|
1094
|
+
return parts.length === 0 ? null : parts.join(", ");
|
|
1095
|
+
}
|
|
1083
1096
|
function registerImportCommand(program) {
|
|
1084
1097
|
const importCmd = program.command("import").description("Import provenance from an external AI tool's native logs");
|
|
1085
1098
|
importCmd.command("claude-code").description("Derive Basou sessions from Claude Code native transcripts (~/.claude/projects)").option(
|
|
@@ -1191,7 +1204,8 @@ async function doRunImportClaudeCode(options, ctx) {
|
|
|
1191
1204
|
store,
|
|
1192
1205
|
candidates,
|
|
1193
1206
|
projectPaths,
|
|
1194
|
-
hasDeclaredBoundary(options, manifest)
|
|
1207
|
+
hasDeclaredBoundary(options, manifest),
|
|
1208
|
+
ctx.onReimportChange
|
|
1195
1209
|
);
|
|
1196
1210
|
}
|
|
1197
1211
|
async function doRunImportCodex(options, ctx) {
|
|
@@ -1227,7 +1241,8 @@ async function doRunImportCodex(options, ctx) {
|
|
|
1227
1241
|
await loadImportStore(paths, options, CODEX_IMPORT_SOURCE),
|
|
1228
1242
|
candidates,
|
|
1229
1243
|
projectPaths,
|
|
1230
|
-
hasDeclaredBoundary(options, manifest)
|
|
1244
|
+
hasDeclaredBoundary(options, manifest),
|
|
1245
|
+
ctx.onReimportChange
|
|
1231
1246
|
);
|
|
1232
1247
|
}
|
|
1233
1248
|
function hasDeclaredBoundary(options, manifest) {
|
|
@@ -1249,7 +1264,7 @@ async function resolveImportTarget(ctx) {
|
|
|
1249
1264
|
const manifest = await readManifest2(paths);
|
|
1250
1265
|
return { repositoryRoot, paths, manifest };
|
|
1251
1266
|
}
|
|
1252
|
-
async function importDerivedSessions(paths, manifest, options, store, candidates, projectPaths, boundaryDeclared) {
|
|
1267
|
+
async function importDerivedSessions(paths, manifest, options, store, candidates, projectPaths, boundaryDeclared, onReimportChange) {
|
|
1253
1268
|
const { byExternalId: existingByExternalId, notDirectories, yamlNotFiles } = store;
|
|
1254
1269
|
let notDirectoriesWarned = false;
|
|
1255
1270
|
const seenThisRun = /* @__PURE__ */ new Set();
|
|
@@ -1279,7 +1294,8 @@ async function importDerivedSessions(paths, manifest, options, store, candidates
|
|
|
1279
1294
|
skippedDecreased: 0,
|
|
1280
1295
|
skippedDuplicate: 0,
|
|
1281
1296
|
skippedUnverifiable: 0,
|
|
1282
|
-
droppedCopies: 0
|
|
1297
|
+
droppedCopies: 0,
|
|
1298
|
+
change: { decisions: 0, outputTokens: 0 }
|
|
1283
1299
|
};
|
|
1284
1300
|
let sanitizedPaths = 0;
|
|
1285
1301
|
const validate = (payload) => {
|
|
@@ -1341,6 +1357,8 @@ async function importDerivedSessions(paths, manifest, options, store, candidates
|
|
|
1341
1357
|
continue;
|
|
1342
1358
|
}
|
|
1343
1359
|
counts.reimported++;
|
|
1360
|
+
counts.change.decisions += outcome.change.decisions;
|
|
1361
|
+
counts.change.outputTokens += outcome.change.outputTokens;
|
|
1344
1362
|
if (holdsCopies?.(prior) === true) counts.droppedCopies++;
|
|
1345
1363
|
seenThisRun.add(externalId);
|
|
1346
1364
|
await noteCrossProject(externalId, payload2);
|
|
@@ -1395,6 +1413,7 @@ async function importDerivedSessions(paths, manifest, options, store, candidates
|
|
|
1395
1413
|
);
|
|
1396
1414
|
}
|
|
1397
1415
|
}
|
|
1416
|
+
onReimportChange?.({ ...counts.change });
|
|
1398
1417
|
printImportResult(options, results, counts);
|
|
1399
1418
|
}
|
|
1400
1419
|
async function classifyReimport(priors, sourcePath, externalId, counts, holdsCopies) {
|
|
@@ -1847,7 +1866,9 @@ function printImportResult(options, results, counts) {
|
|
|
1847
1866
|
skipParts.push(`${skippedUnverifiable} unverifiable (run 'basou verify')`);
|
|
1848
1867
|
const skipSuffix = skipParts.length > 0 ? `; skipped ${skipParts.join(", ")}` : "";
|
|
1849
1868
|
const eventsPart = replaced > 0 ? `${eventTotal} events, ${replaced} replaced` : `${eventTotal} events`;
|
|
1850
|
-
const
|
|
1869
|
+
const copies = droppedCopies > 0 ? `${droppedCopies} without the records ${droppedCopies === 1 ? "its transcript" : "their transcripts"} copied from an older one` : null;
|
|
1870
|
+
const notes = [copies, reimportFallText(counts.change)].filter((n) => n !== null);
|
|
1871
|
+
const copiesPart = notes.length > 0 ? ` (${notes.join("; ")})` : "";
|
|
1851
1872
|
if (isDry) {
|
|
1852
1873
|
const parts = [];
|
|
1853
1874
|
if (results.length > 0) parts.push(`import ${results.length} session(s) (${eventsPart})`);
|
|
@@ -1946,10 +1967,23 @@ function isMissingSourceDir(error) {
|
|
|
1946
1967
|
if (!(error instanceof Error)) return false;
|
|
1947
1968
|
return error.message === "Claude transcript directory not found for project" || error.message === "Codex sessions directory not found";
|
|
1948
1969
|
}
|
|
1970
|
+
var reimportChanges = /* @__PURE__ */ new WeakMap();
|
|
1971
|
+
function reimportChangeOf(outcome) {
|
|
1972
|
+
return reimportChanges.get(outcome);
|
|
1973
|
+
}
|
|
1974
|
+
function noteReimportChange(outcome, change) {
|
|
1975
|
+
reimportChanges.set(outcome, change);
|
|
1976
|
+
return outcome;
|
|
1977
|
+
}
|
|
1949
1978
|
async function runImport(adapter, fn) {
|
|
1950
1979
|
try {
|
|
1951
|
-
|
|
1952
|
-
|
|
1980
|
+
let reimportChange = { decisions: 0, outputTokens: 0 };
|
|
1981
|
+
const json = await captureImportJson(
|
|
1982
|
+
() => fn((change) => {
|
|
1983
|
+
reimportChange = change;
|
|
1984
|
+
})
|
|
1985
|
+
);
|
|
1986
|
+
const outcome = {
|
|
1953
1987
|
adapter,
|
|
1954
1988
|
status: "ran",
|
|
1955
1989
|
importedCount: readCount(json.imported_count),
|
|
@@ -1965,6 +1999,7 @@ async function runImport(adapter, fn) {
|
|
|
1965
1999
|
eventTotal: readCount(json.event_total),
|
|
1966
2000
|
dryRun: json.dry_run === true
|
|
1967
2001
|
};
|
|
2002
|
+
return noteReimportChange(outcome, reimportChange);
|
|
1968
2003
|
} catch (error) {
|
|
1969
2004
|
if (isMissingSourceDir(error)) {
|
|
1970
2005
|
return { adapter, status: "skipped", reason: "no source logs for this project" };
|
|
@@ -1982,10 +2017,16 @@ function importOptions(options) {
|
|
|
1982
2017
|
};
|
|
1983
2018
|
}
|
|
1984
2019
|
function importClaudeCode(options, ctx) {
|
|
1985
|
-
return runImport(
|
|
2020
|
+
return runImport(
|
|
2021
|
+
"claude-code",
|
|
2022
|
+
(onReimportChange) => doRunImportClaudeCode(importOptions(options), { ...ctx, onReimportChange })
|
|
2023
|
+
);
|
|
1986
2024
|
}
|
|
1987
2025
|
function importCodex(options, ctx) {
|
|
1988
|
-
return runImport(
|
|
2026
|
+
return runImport(
|
|
2027
|
+
"codex",
|
|
2028
|
+
(onReimportChange) => doRunImportCodex(importOptions(options), { ...ctx, onReimportChange })
|
|
2029
|
+
);
|
|
1989
2030
|
}
|
|
1990
2031
|
async function regenerateHandoff(paths, nowIso, callbacks) {
|
|
1991
2032
|
const result = await renderHandoff({ paths, nowIso, ...callbacks });
|
|
@@ -2809,11 +2850,14 @@ function diffLines(diff, more) {
|
|
|
2809
2850
|
` ${displayPath3(c.at)}: ${shownJson(c.before)} -> ${shownJson(c.after)}${delta}${flag(c)}`
|
|
2810
2851
|
);
|
|
2811
2852
|
}
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
}
|
|
2853
|
+
const only = (sign, c) => {
|
|
2854
|
+
const entry = setEntryOf(c.at);
|
|
2855
|
+
if (entry === null) return ` ${sign} ${displayPath3(c.at)}: ${shownJson(c.value)}${flag(c)}`;
|
|
2856
|
+
const how = sign === "+" ? "now lists" : "no longer lists";
|
|
2857
|
+
return ` ${sign} ${displayPath3(entry.list)}: ${how} ${shownJson(entry.name)}${flag(c)}`;
|
|
2858
|
+
};
|
|
2859
|
+
for (const c of diff.added) lines.push(only("+", c));
|
|
2860
|
+
for (const c of diff.removed) lines.push(only("-", c));
|
|
2817
2861
|
if (lines.length === 0 && !more) lines.push(" nothing moved");
|
|
2818
2862
|
return lines;
|
|
2819
2863
|
}
|
|
@@ -10229,8 +10273,13 @@ function describeImport(outcome) {
|
|
|
10229
10273
|
const verb = outcome.dryRun ? "would import" : "imported";
|
|
10230
10274
|
const parts = [`${outcome.importedCount} session(s)`, `${outcome.eventTotal} events`];
|
|
10231
10275
|
if (outcome.reimportedCount > 0) {
|
|
10232
|
-
const
|
|
10233
|
-
|
|
10276
|
+
const notes = [
|
|
10277
|
+
outcome.droppedCopiesCount > 0 ? `${outcome.droppedCopiesCount} without records copied from an older transcript` : null,
|
|
10278
|
+
reimportFallText(reimportChangeOf(outcome) ?? { decisions: 0, outputTokens: 0 })
|
|
10279
|
+
].filter((n) => n !== null);
|
|
10280
|
+
parts.push(
|
|
10281
|
+
`${outcome.reimportedCount} re-imported${notes.length > 0 ? ` (${notes.join("; ")})` : ""}`
|
|
10282
|
+
);
|
|
10234
10283
|
}
|
|
10235
10284
|
if (outcome.replacedCount > 0) parts.push(`${outcome.replacedCount} replaced`);
|
|
10236
10285
|
if (outcome.skippedAlreadyImported > 0)
|
|
@@ -13964,12 +14013,31 @@ var BOARD_HTML = `<!doctype html>
|
|
|
13964
14013
|
table { border-collapse: collapse; }
|
|
13965
14014
|
th, td { text-align: left; vertical-align: top; padding: 4px 10px 4px 0; }
|
|
13966
14015
|
td.n, th.n { text-align: right; font-variant-numeric: tabular-nums; }
|
|
14016
|
+
/* A number is never broken across lines, so a part of it cannot pass for another number. */
|
|
14017
|
+
td.n { white-space: nowrap; }
|
|
13967
14018
|
table.grid td, table.grid th { border-bottom: 1px solid var(--grid); }
|
|
13968
|
-
|
|
14019
|
+
/* Where a table is wider than its box, a shade at an edge says there is
|
|
14020
|
+
more that way, also where scrollbars are drawn over the content and
|
|
14021
|
+
hidden: each shade stays at its edge, and a cover of the page's color,
|
|
14022
|
+
moving with the content, hides it while that end is in view. */
|
|
14023
|
+
.scroll {
|
|
14024
|
+
overflow-x: auto; max-width: 100%;
|
|
14025
|
+
background:
|
|
14026
|
+
linear-gradient(to right, var(--bg) 40%, transparent) left center / 28px 100% no-repeat local,
|
|
14027
|
+
linear-gradient(to left, var(--bg) 40%, transparent) right center / 28px 100% no-repeat local,
|
|
14028
|
+
radial-gradient(farthest-side at 0 50%, color-mix(in srgb, var(--fg-2) 40%, transparent), transparent) left center / 12px 100% no-repeat scroll,
|
|
14029
|
+
radial-gradient(farthest-side at 100% 50%, color-mix(in srgb, var(--fg-2) 40%, transparent), transparent) right center / 12px 100% no-repeat scroll;
|
|
14030
|
+
}
|
|
14031
|
+
/* Lane names, and the heads of the repos' table, break between words, not
|
|
14032
|
+
inside one (a name in Japanese is one word). */
|
|
14033
|
+
td.lanes, table.repos th { word-break: keep-all; }
|
|
13969
14034
|
table.matrix th.stage { font-weight: 400; font-size: 12px; text-align: center; min-width: 64px; }
|
|
13970
14035
|
table.matrix th.stage b { display: block; font-size: 13px; }
|
|
13971
14036
|
table.matrix td.cell { text-align: center; font-size: 18px; line-height: 1; }
|
|
13972
14037
|
.stage-meanings { display: none; list-style: none; padding: 0; margin: 6px 0 0; font-size: 12px; color: var(--fg-2); }
|
|
14038
|
+
/* Drawn only below about 600px, by the rule below. */
|
|
14039
|
+
tr.source { display: none; }
|
|
14040
|
+
tr.source td { padding-top: 0; }
|
|
13973
14041
|
/* Six stages do not fit beside their meanings below about 600px: each is
|
|
13974
14042
|
drawn by its id, the meanings are listed under the matrix, and a lane
|
|
13975
14043
|
name wraps. After the rules above, which it overrides. */
|
|
@@ -13985,8 +14053,13 @@ var BOARD_HTML = `<!doctype html>
|
|
|
13985
14053
|
is cut off. The cell keeps no right padding, which a sticky note could
|
|
13986
14054
|
not cross. */
|
|
13987
14055
|
.scroll { container-type: inline-size; }
|
|
13988
|
-
tr.notes td { padding-right: 0; }
|
|
13989
|
-
tr.notes ul.sub { position: sticky; left: 0; max-width: calc(100vw - 32px); max-width: 100cqw; }
|
|
14056
|
+
tr.notes td, tr.source td { padding-right: 0; }
|
|
14057
|
+
tr.notes ul.sub, tr.source div.sub { position: sticky; left: 0; max-width: calc(100vw - 32px); max-width: 100cqw; }
|
|
14058
|
+
/* An observation's source is drawn in a row of its own under it, where
|
|
14059
|
+
it wraps at the width in view, not in a column off the screen that
|
|
14060
|
+
makes its row tall. */
|
|
14061
|
+
th.source, td.source { display: none; }
|
|
14062
|
+
tr.source { display: table-row; }
|
|
13990
14063
|
}
|
|
13991
14064
|
.st::before { display: inline-block; width: 1.2em; }
|
|
13992
14065
|
.st.done::before { content: "\\25CF"; color: var(--done); }
|
|
@@ -13995,7 +14068,9 @@ var BOARD_HTML = `<!doctype html>
|
|
|
13995
14068
|
.st.shelved::before { content: "\\2298"; color: var(--fg-2); }
|
|
13996
14069
|
.st.none::before { content: "\\25CB"; color: var(--fg-2); }
|
|
13997
14070
|
.st.unverified::before { content: "?"; color: var(--info); font-weight: 700; }
|
|
13998
|
-
td.moved { outline: 2px solid var(--info); outline-offset: -3px; border-radius: 6px; }
|
|
14071
|
+
td.moved, .moved-key { outline: 2px solid var(--info); outline-offset: -3px; border-radius: 6px; }
|
|
14072
|
+
.moved-key { display: inline-block; width: 1.2em; height: 1.2em; margin-right: 4px; vertical-align: -0.3em; }
|
|
14073
|
+
ul.moved { margin: 6px 0 0; padding-left: 18px; font-size: 12px; }
|
|
13999
14074
|
.legend { font-size: 12px; color: var(--fg-2); display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 6px; }
|
|
14000
14075
|
.swatch { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 4px; vertical-align: -1px; }
|
|
14001
14076
|
.swatch.s1 { background: var(--s1); }
|
|
@@ -14013,6 +14088,7 @@ var BOARD_HTML = `<!doctype html>
|
|
|
14013
14088
|
td.wrap { overflow-wrap: anywhere; min-width: 10em; }
|
|
14014
14089
|
tr.notes td { padding-top: 0; }
|
|
14015
14090
|
.chip.changed { margin-left: 0; margin-right: 6px; background: var(--chip-unverified); }
|
|
14091
|
+
.chip.added { margin-left: 0; margin-right: 6px; background: var(--chip-live); }
|
|
14016
14092
|
.bar { height: 10px; background: var(--fill); border-radius: 5px; min-width: 120px; position: relative; overflow: hidden; }
|
|
14017
14093
|
.bar span { position: absolute; left: 0; top: 0; bottom: 0; background: var(--s1); }
|
|
14018
14094
|
figure { margin: 16px 0; }
|
|
@@ -14033,11 +14109,16 @@ var BOARD_HTML = `<!doctype html>
|
|
|
14033
14109
|
ol.turns li { margin-bottom: 8px; }
|
|
14034
14110
|
.foot { font-size: 13px; }
|
|
14035
14111
|
#status.err { color: var(--err); }
|
|
14112
|
+
/* The request to copy breaks anywhere, so that a narrow screen keeps all of it in view. */
|
|
14113
|
+
.ask { font-size: 13px; color: var(--fg-2); margin-bottom: 8px; }
|
|
14114
|
+
.ask code { color: var(--fg); overflow-wrap: anywhere; padding: 0 3px; border-radius: 4px; background: var(--fill); }
|
|
14115
|
+
.ask button { font: inherit; margin: 0 6px; }
|
|
14036
14116
|
</style>
|
|
14037
14117
|
</head>
|
|
14038
14118
|
<body>
|
|
14039
14119
|
<div class="wrap">
|
|
14040
14120
|
<nav class="records" id="records"></nav>
|
|
14121
|
+
<div class="ask" id="ask"></div>
|
|
14041
14122
|
<div id="status"></div>
|
|
14042
14123
|
<div id="board"></div>
|
|
14043
14124
|
</div>
|
|
@@ -14158,11 +14239,27 @@ var BOARD_HTML = `<!doctype html>
|
|
|
14158
14239
|
var DOT = ' ' + String.fromCharCode(183) + ' ';
|
|
14159
14240
|
var FOUND = String.fromCharCode(10003);
|
|
14160
14241
|
var NOT_FOUND = String.fromCharCode(10007);
|
|
14242
|
+
// Whether a final_url is where the declared url points: the same origin and
|
|
14243
|
+
// path, and nothing more. A final_url written from basou 0.73.0 holds no
|
|
14244
|
+
// query, fragment, user or password; one in a record written before may, and
|
|
14245
|
+
// is then drawn, as what it holds can be what moved. A test lifts this function.
|
|
14246
|
+
function landsAsDeclared(url, finalUrl) {
|
|
14247
|
+
try {
|
|
14248
|
+
var a = new URL(url);
|
|
14249
|
+
var b = new URL(finalUrl);
|
|
14250
|
+
return a.origin === b.origin && a.pathname === b.pathname &&
|
|
14251
|
+
b.search === '' && b.hash === '' && b.username === '' && b.password === '';
|
|
14252
|
+
} catch (e) {
|
|
14253
|
+
return false;
|
|
14254
|
+
}
|
|
14255
|
+
}
|
|
14161
14256
|
// An http value read as its parts: the status, the redirects, where it
|
|
14162
|
-
// landed
|
|
14257
|
+
// landed (left out where it is the url declared, which is drawn under the
|
|
14258
|
+
// name), and what it read off the body (when the status is 2xx) or that it
|
|
14163
14259
|
// did not. Numbers and strings are drawn as they are.
|
|
14164
|
-
function httpText(h) {
|
|
14165
|
-
var parts = [String(h.status), fill(S.observed.redirects, { n: h.redirects })
|
|
14260
|
+
function httpText(h, url) {
|
|
14261
|
+
var parts = [String(h.status), fill(S.observed.redirects, { n: h.redirects })];
|
|
14262
|
+
if (!url || !landsAsDeclared(url, h.final_url)) parts.push(h.final_url);
|
|
14166
14263
|
if (h.body === null) parts.push(S.observed.bodyNotRead);
|
|
14167
14264
|
else if (h.body) {
|
|
14168
14265
|
if (h.body.words) {
|
|
@@ -14177,23 +14274,35 @@ var BOARD_HTML = `<!doctype html>
|
|
|
14177
14274
|
}
|
|
14178
14275
|
// A value of an observation as the page draws it: an http value in its
|
|
14179
14276
|
// shape read as its parts, one with words beside what it says of each, and
|
|
14180
|
-
// any other as it is (a string) or as JSON.
|
|
14181
|
-
|
|
14182
|
-
|
|
14277
|
+
// any other as it is (a string) or as JSON. url: the url its declaration
|
|
14278
|
+
// gives, or null. A test lifts this function.
|
|
14279
|
+
function observedText(value, words, http, url) {
|
|
14280
|
+
if (http) return httpText(http, url);
|
|
14183
14281
|
if (words) return withWords(value, words);
|
|
14184
14282
|
return shown(value);
|
|
14185
14283
|
}
|
|
14284
|
+
// The url an http observation's declaration gives, or null.
|
|
14285
|
+
function declaredUrl(o) {
|
|
14286
|
+
return o.kind === 'http' && o.looks_at && o.looks_at.length > 0 ? o.looks_at[0] : null;
|
|
14287
|
+
}
|
|
14288
|
+
// When an observation was made, as the page draws a time; a day alone (a
|
|
14289
|
+
// manual observation may give one) is drawn as it is, not as its midnight.
|
|
14290
|
+
// A test lifts this function.
|
|
14291
|
+
function observedAtText(at) {
|
|
14292
|
+
return /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(String(at)) ? String(at) : when(at);
|
|
14293
|
+
}
|
|
14186
14294
|
// The value of an observation that moved, and what the previous record
|
|
14187
14295
|
// held: a value whose type alone moved ("12" and 12) is drawn as JSON, so
|
|
14188
14296
|
// that the two do not read the same. A test lifts this function.
|
|
14189
14297
|
function movedText(o) {
|
|
14190
14298
|
var m = o.moved;
|
|
14191
|
-
var
|
|
14299
|
+
var url = declaredUrl(o);
|
|
14300
|
+
var now = observedText(o.value, o.words, o.http, url);
|
|
14192
14301
|
if (m.status === 'none') return { now: now, then: S.observed.beforeNone };
|
|
14193
14302
|
if (m.value === null) {
|
|
14194
14303
|
return { now: now, then: S.observed.beforeNotObserved + (m.error ? ' (' + m.error + ')' : '') };
|
|
14195
14304
|
}
|
|
14196
|
-
var then = observedText(m.value, m.words, m.http);
|
|
14305
|
+
var then = observedText(m.value, m.words, m.http, url);
|
|
14197
14306
|
if ((typeof m.value === 'string') !== (typeof o.value === 'string')) {
|
|
14198
14307
|
then = JSON.stringify(m.value);
|
|
14199
14308
|
if (then === now) now = JSON.stringify(o.value);
|
|
@@ -14227,6 +14336,59 @@ var BOARD_HTML = `<!doctype html>
|
|
|
14227
14336
|
var list = page.records || [];
|
|
14228
14337
|
if (list.length > 0) recordLinks(nav, page, list);
|
|
14229
14338
|
nav.appendChild(el('a', { href: boardHref('live=1'), text: S.live.measureNow }));
|
|
14339
|
+
askToRecord(list.length > 0);
|
|
14340
|
+
}
|
|
14341
|
+
|
|
14342
|
+
// The page records nothing, measuring now included: an agent judges the
|
|
14343
|
+
// board and records it. show: whether the workspace has records, where the
|
|
14344
|
+
// page says so with the request to copy into a session of the workspace.
|
|
14345
|
+
function askToRecord(show) {
|
|
14346
|
+
var box = $('ask');
|
|
14347
|
+
clear(box);
|
|
14348
|
+
if (!show) return;
|
|
14349
|
+
var result = el('span', { role: 'status' });
|
|
14350
|
+
var copy = el('button', { type: 'button', text: S.ask.copy, onclick: function () { copyRequest(result); } });
|
|
14351
|
+
var lead = fill(S.ask.lead, { measureNow: S.live.measureNow }) + ' ';
|
|
14352
|
+
[document.createTextNode(lead), el('code', { text: S.ask.request }), copy, result]
|
|
14353
|
+
.forEach(function (node) { box.appendChild(node); });
|
|
14354
|
+
}
|
|
14355
|
+
// A browser gives the clipboard to a page of 127.0.0.1, which it counts as
|
|
14356
|
+
// secure, but it can refuse the permission to write to it, as a browser
|
|
14357
|
+
// built into an app does. Then the request is copied as a selection is,
|
|
14358
|
+
// which the click still allows. Where neither copies, say so: the request
|
|
14359
|
+
// stays on the page to select.
|
|
14360
|
+
function copyRequest(result) {
|
|
14361
|
+
var say = function (text) { result.textContent = text; };
|
|
14362
|
+
var bySelection = function () { say(copySelected(S.ask.request) ? S.ask.copied : S.ask.copyFailed); };
|
|
14363
|
+
var clip = typeof navigator !== 'undefined' && navigator.clipboard ? navigator.clipboard : null;
|
|
14364
|
+
if (clip === null) {
|
|
14365
|
+
bySelection();
|
|
14366
|
+
return;
|
|
14367
|
+
}
|
|
14368
|
+
try {
|
|
14369
|
+
clip.writeText(S.ask.request).then(function () { say(S.ask.copied); }, bySelection);
|
|
14370
|
+
} catch (err) {
|
|
14371
|
+
bySelection();
|
|
14372
|
+
}
|
|
14373
|
+
}
|
|
14374
|
+
// Copy text as a selection is copied, from a field put in for it and taken
|
|
14375
|
+
// out again. False where the browser does not say it copied.
|
|
14376
|
+
function copySelected(text) {
|
|
14377
|
+
if (typeof document.execCommand !== 'function' || !document.body) return false;
|
|
14378
|
+
var field = el('textarea', { readonly: '', 'aria-hidden': 'true' });
|
|
14379
|
+
field.value = text;
|
|
14380
|
+
field.style.position = 'fixed';
|
|
14381
|
+
field.style.opacity = '0';
|
|
14382
|
+
document.body.appendChild(field);
|
|
14383
|
+
var copied = false;
|
|
14384
|
+
try {
|
|
14385
|
+
field.select();
|
|
14386
|
+
copied = document.execCommand('copy') === true;
|
|
14387
|
+
} catch (err) {
|
|
14388
|
+
copied = false;
|
|
14389
|
+
}
|
|
14390
|
+
document.body.removeChild(field);
|
|
14391
|
+
return copied;
|
|
14230
14392
|
}
|
|
14231
14393
|
|
|
14232
14394
|
function recordLinks(nav, page, list) {
|
|
@@ -14303,14 +14465,19 @@ var BOARD_HTML = `<!doctype html>
|
|
|
14303
14465
|
s.observed.forEach(function (o) {
|
|
14304
14466
|
var cell = el('td', { class: 'wrap' });
|
|
14305
14467
|
var value;
|
|
14306
|
-
if (o.value !== null && o.moved) {
|
|
14468
|
+
if (o.value !== null && o.moved && o.moved.status === 'none') {
|
|
14469
|
+
// Declared since the previous record: new, which is not a change.
|
|
14470
|
+
cell.appendChild(el('span', { class: 'chip added', text: S.observed.added }));
|
|
14471
|
+
value = observedText(o.value, o.words, o.http, declaredUrl(o));
|
|
14472
|
+
cell.appendChild(el('span', { text: value + (o.error ? ' (' + o.error + ')' : '') }));
|
|
14473
|
+
} else if (o.value !== null && o.moved) {
|
|
14307
14474
|
var pair = movedText(o);
|
|
14308
14475
|
cell.appendChild(el('span', { class: 'chip changed', text: S.observed.changed }));
|
|
14309
14476
|
value = pair.now;
|
|
14310
14477
|
cell.appendChild(el('span', { text: value + (o.error ? ' (' + o.error + ')' : '') }));
|
|
14311
14478
|
cell.appendChild(el('div', { class: 'sub', text: pair.then }));
|
|
14312
14479
|
} else {
|
|
14313
|
-
if (o.value !== null) value = observedText(o.value, o.words, o.http);
|
|
14480
|
+
if (o.value !== null) value = observedText(o.value, o.words, o.http, declaredUrl(o));
|
|
14314
14481
|
else if (o.previous && o.previous.status === 'value') value = fill(S.observed.missing, { value: shown(o.previous.value) });
|
|
14315
14482
|
else if (o.previous && o.previous.status === 'unreadable') value = S.observed.missingPreviousUnreadable;
|
|
14316
14483
|
else value = S.observed.missingNoPrevious;
|
|
@@ -14322,8 +14489,13 @@ var BOARD_HTML = `<!doctype html>
|
|
|
14322
14489
|
rows.push(el('tr', null, [
|
|
14323
14490
|
el('td', null, [el('div', { text: o.name }), what === null ? null : el('div', { class: 'sub', text: what })]),
|
|
14324
14491
|
cell,
|
|
14325
|
-
el('td', { text: o.observed_at
|
|
14492
|
+
el('td', { text: observedAtText(o.observed_at), title: o.observed_at }),
|
|
14493
|
+
el('td', { class: 'wrap source', text: o.source })
|
|
14326
14494
|
]));
|
|
14495
|
+
// Its source again, drawn only on a narrow screen, where its column is not.
|
|
14496
|
+
rows.push(el('tr', { class: 'source' }, [el('td', { colspan: '4' }, [
|
|
14497
|
+
el('div', { class: 'sub', text: S.observed.source + ': ' + o.source })
|
|
14498
|
+
])]));
|
|
14327
14499
|
if (o.notes && o.notes.length > 0) {
|
|
14328
14500
|
rows.push(el('tr', { class: 'notes' }, [el('td', { colspan: '4' }, [
|
|
14329
14501
|
el('ul', { class: 'sub' }, o.notes.map(function (n) { return el('li', null, [prose(n, 'span')]); }))
|
|
@@ -14333,7 +14505,7 @@ var BOARD_HTML = `<!doctype html>
|
|
|
14333
14505
|
observed = el('div', { class: 'scroll' }, [el('table', { class: 'grid' }, [
|
|
14334
14506
|
el('tr', null, [
|
|
14335
14507
|
el('th', { text: S.observed.name }), el('th', { text: S.observed.value }),
|
|
14336
|
-
el('th', { text: S.observed.observedAt }), el('th', { text: S.observed.source })
|
|
14508
|
+
el('th', { text: S.observed.observedAt }), el('th', { class: 'source', text: S.observed.source })
|
|
14337
14509
|
])
|
|
14338
14510
|
].concat(rows))]);
|
|
14339
14511
|
}
|
|
@@ -14664,7 +14836,22 @@ var BOARD_HTML = `<!doctype html>
|
|
|
14664
14836
|
['done', 'part', 'blocked', 'shelved', 'none', 'unverified'].forEach(function (s) {
|
|
14665
14837
|
legend.appendChild(el('span', null, [el('span', { class: 'st ' + s }), stateName(s)]));
|
|
14666
14838
|
});
|
|
14839
|
+
// The cells that moved since the previous record: their mark in the
|
|
14840
|
+
// legend, and each with the state it had, which a cell's title alone says
|
|
14841
|
+
// only to a pointer that hovers.
|
|
14842
|
+
var moved = [];
|
|
14843
|
+
x.lanes.forEach(function (lane) {
|
|
14844
|
+
lane.cells.forEach(function (c, i) {
|
|
14845
|
+
if (c.moved_from) moved.push({ lane: lane.name, stage: x.stages[i] ? x.stages[i].id : '', before: c.moved_from, after: c.state });
|
|
14846
|
+
});
|
|
14847
|
+
});
|
|
14848
|
+
if (moved.length > 0) legend.appendChild(el('span', null, [el('span', { class: 'moved-key' }), S.matrix.movedKey]));
|
|
14667
14849
|
children.push(legend);
|
|
14850
|
+
if (moved.length > 0) {
|
|
14851
|
+
children.push(el('ul', { class: 'moved' }, moved.map(function (m) {
|
|
14852
|
+
return el('li', { text: fill(S.matrix.movedCell, { lane: m.lane, stage: m.stage, before: stateName(m.before), after: stateName(m.after) }) });
|
|
14853
|
+
})));
|
|
14854
|
+
}
|
|
14668
14855
|
return section(S.sections.matrix, true, children);
|
|
14669
14856
|
}
|
|
14670
14857
|
|
|
@@ -14713,16 +14900,18 @@ var BOARD_HTML = `<!doctype html>
|
|
|
14713
14900
|
if (b.measures.length === 0) {
|
|
14714
14901
|
return section(S.sections.measures, false, [el('p', { class: 'muted', text: S.measures.none })]);
|
|
14715
14902
|
}
|
|
14903
|
+
// How much a number moved, right beside it: where the table is wider than
|
|
14904
|
+
// the screen, the lanes, which are words, are the column cut off.
|
|
14716
14905
|
var head = el('tr', null, [
|
|
14717
14906
|
el('th', { text: S.measures.id }), el('th', { text: S.measures.value }),
|
|
14718
|
-
el('th', { text: S.measures.
|
|
14907
|
+
el('th', { class: 'n', text: S.measures.delta }), el('th', { text: S.measures.lanes })
|
|
14719
14908
|
]);
|
|
14720
14909
|
var rows = b.measures.map(function (m) {
|
|
14721
14910
|
return el('tr', { 'data-measure': m.id }, [
|
|
14722
14911
|
el('td', null, [el('code', { text: m.id })]),
|
|
14723
14912
|
el('td', { class: 'wrap', text: measureText(m) }),
|
|
14724
|
-
el('td', { text: m.
|
|
14725
|
-
el('td', { class: '
|
|
14913
|
+
el('td', { class: 'n', text: m.delta === null || m.delta === undefined ? '' : signed(m.delta) }),
|
|
14914
|
+
el('td', { class: 'lanes', text: m.lanes.length === 0 ? '-' : m.lanes.join(', ') })
|
|
14726
14915
|
]);
|
|
14727
14916
|
});
|
|
14728
14917
|
return section(S.sections.measures, false, [el('div', { class: 'scroll' }, [el('table', { class: 'grid' }, [head].concat(rows))])]);
|
|
@@ -14816,9 +15005,10 @@ var BOARD_HTML = `<!doctype html>
|
|
|
14816
15005
|
// The board's own title, so that boards open in other tabs can be told apart.
|
|
14817
15006
|
document.title = b.heading.title;
|
|
14818
15007
|
// Where each lane stops, right under the summary's tiles, and the lanes,
|
|
14819
|
-
// before the observations and the charts of the effort
|
|
14820
|
-
|
|
14821
|
-
|
|
15008
|
+
// before the observations and the charts of the effort; then the
|
|
15009
|
+
// operator's turns, which are read most, before the tables of every value.
|
|
15010
|
+
[heading(b), summary(b), matrix(b), lanes(b), observations(b), effort(b, false), turns(b), measures(b),
|
|
15011
|
+
composition(b), recordRepos(b), recordComponents(b), footnotes(b)]
|
|
14822
15012
|
.forEach(function (node) { root.appendChild(node); });
|
|
14823
15013
|
}
|
|
14824
15014
|
|
|
@@ -14875,7 +15065,7 @@ var BOARD_HTML = `<!doctype html>
|
|
|
14875
15065
|
el('td', { text: unmeasured('fetched_at', r.fetched_at, when) })
|
|
14876
15066
|
]);
|
|
14877
15067
|
});
|
|
14878
|
-
return el('div', { class: 'scroll' }, [el('table', { class: 'grid' }, [head].concat(rows))]);
|
|
15068
|
+
return el('div', { class: 'scroll' }, [el('table', { class: 'grid repos' }, [head].concat(rows))]);
|
|
14879
15069
|
}
|
|
14880
15070
|
function liveRepos(l) {
|
|
14881
15071
|
return section(S.live.repos, false, [reposTable(l.repos, missingIn(l), false)]);
|
|
@@ -14972,9 +15162,12 @@ var BOARD_HTML = `<!doctype html>
|
|
|
14972
15162
|
}));
|
|
14973
15163
|
return section(S.live.components, false, [body]);
|
|
14974
15164
|
}
|
|
14975
|
-
|
|
15165
|
+
// noRecord: whether to say how a record comes to be drawn, which is said
|
|
15166
|
+
// only where the workspace has no record.
|
|
15167
|
+
function liveFootnotes(l, why, noRecord) {
|
|
14976
15168
|
var f = l.footnotes;
|
|
14977
|
-
var items = [el('li', { text: S.live.note })
|
|
15169
|
+
var items = [el('li', { text: S.live.note })];
|
|
15170
|
+
if (noRecord) items.push(el('li', { text: S.live.judged }));
|
|
14978
15171
|
if (why === 'no_board') items.push(el('li', { text: S.unavailable.noBoard }));
|
|
14979
15172
|
if (f.not_found.length > 0) {
|
|
14980
15173
|
items.push(el('li', null, [el('b', { text: S.footnotes.notMeasured }), el('ul', null, f.not_found.map(function (n) {
|
|
@@ -14995,19 +15188,30 @@ var BOARD_HTML = `<!doctype html>
|
|
|
14995
15188
|
var drawn = ++liveDrawn;
|
|
14996
15189
|
var nav = $('records');
|
|
14997
15190
|
clear(nav);
|
|
14998
|
-
|
|
14999
|
-
if (why === null) {
|
|
15000
|
-
getJson(apiPath('board')).then(function (b) {
|
|
15001
|
-
var list = b && b.page && b.page.records ? b.page.records : [];
|
|
15002
|
-
if (drawn === liveDrawn && list.length > 0) nav.appendChild(el('a', { href: boardHref(''), text: S.live.toRecords }));
|
|
15003
|
-
}).catch(function () {});
|
|
15004
|
-
}
|
|
15191
|
+
askToRecord(false);
|
|
15005
15192
|
var root = $('board');
|
|
15006
15193
|
clear(root);
|
|
15007
15194
|
setStatus('', false);
|
|
15008
15195
|
var l = d.live;
|
|
15009
|
-
|
|
15196
|
+
var foot = liveFootnotes(l, why, why !== null);
|
|
15197
|
+
[liveHeading(l, why), effort(l, true), liveRepos(l), liveTrail(l), liveComponents(l), foot]
|
|
15010
15198
|
.forEach(function (node) { root.appendChild(node); });
|
|
15199
|
+
// Asked for with ?live=1: lead back to the records when there are any,
|
|
15200
|
+
// saying how to record the board again, and say how one comes to be
|
|
15201
|
+
// drawn when there are none.
|
|
15202
|
+
if (why === null) {
|
|
15203
|
+
getJson(apiPath('board')).then(function (b) {
|
|
15204
|
+
var list = b && b.page && b.page.records ? b.page.records : [];
|
|
15205
|
+
if (drawn !== liveDrawn) return;
|
|
15206
|
+
if (list.length > 0) {
|
|
15207
|
+
nav.appendChild(el('a', { href: boardHref(''), text: S.live.toRecords }));
|
|
15208
|
+
askToRecord(true);
|
|
15209
|
+
} else {
|
|
15210
|
+
root.removeChild(foot);
|
|
15211
|
+
root.appendChild(liveFootnotes(l, why, true));
|
|
15212
|
+
}
|
|
15213
|
+
}).catch(function () {});
|
|
15214
|
+
}
|
|
15011
15215
|
}
|
|
15012
15216
|
|
|
15013
15217
|
function getJson(url) {
|
|
@@ -15369,8 +15573,12 @@ var VIEW_HTML = `<!doctype html>
|
|
|
15369
15573
|
if (!st.checked) return el('span', { class: 'badge', text: 'freshness unknown' });
|
|
15370
15574
|
if (st.unverifiableSessions > 0)
|
|
15371
15575
|
return el('span', { class: 'badge danger', text: '\u26A0 ' + st.unverifiableSessions + ' unverifiable \u2014 run verify' });
|
|
15372
|
-
|
|
15373
|
-
|
|
15576
|
+
// The updated ones are said beside the uncaptured ones, so the badge counts
|
|
15577
|
+
// what measure's freshness does ("1 new, 1 updated").
|
|
15578
|
+
if (st.newSessions > 0) {
|
|
15579
|
+
var updated = st.updatedSessions > 0 ? ', ' + st.updatedSessions + ' updated' : '';
|
|
15580
|
+
return el('span', { class: 'badge danger', text: '\u26A0 ' + st.newSessions + ' uncaptured' + updated + ' \u2014 run refresh' });
|
|
15581
|
+
}
|
|
15374
15582
|
if (st.updatedSessions > 0)
|
|
15375
15583
|
return el('span', { class: 'badge warn', text: st.updatedSessions + ' updated \u2014 run refresh' });
|
|
15376
15584
|
return el('span', { class: 'badge ok', text: 'up to date' });
|
|
@@ -16593,7 +16801,7 @@ function installExperimentalNotice(program, names = EXPERIMENTAL_COMMANDS, write
|
|
|
16593
16801
|
function readBuildStamp() {
|
|
16594
16802
|
if (false) return void 0;
|
|
16595
16803
|
try {
|
|
16596
|
-
return JSON.parse('{"version":"0.
|
|
16804
|
+
return JSON.parse('{"version":"0.75.0","commit":"6de9730","committedAt":"2026-11-02T12:08:26+09:00"}');
|
|
16597
16805
|
} catch {
|
|
16598
16806
|
return void 0;
|
|
16599
16807
|
}
|