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