p-backlog 0.5.1 → 0.6.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/CHANGELOG.md +13 -0
- package/README.md +4 -0
- package/README.ru.md +5 -0
- package/dist/cli.js +546 -233
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -42,7 +42,7 @@ function formatLocalIso(date) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// src/core/store/runs.ts
|
|
45
|
-
import {
|
|
45
|
+
import { mkdir } from "node:fs/promises";
|
|
46
46
|
import { join as join3 } from "node:path";
|
|
47
47
|
import { z } from "zod";
|
|
48
48
|
|
|
@@ -201,7 +201,7 @@ import { setTimeout as sleep2 } from "node:timers/promises";
|
|
|
201
201
|
|
|
202
202
|
// src/core/store/fs-utils.ts
|
|
203
203
|
import { createHash, randomUUID } from "node:crypto";
|
|
204
|
-
import { chmod, link, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
204
|
+
import { chmod, link, open, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
205
205
|
import { basename, dirname, join } from "node:path";
|
|
206
206
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
207
207
|
function contentVersion(text) {
|
|
@@ -215,16 +215,56 @@ async function readTextOrNull(path) {
|
|
|
215
215
|
throw error;
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
|
-
|
|
219
|
-
const text = await readTextOrNull(path) ?? "";
|
|
218
|
+
function parseJsonLines(text, schema) {
|
|
220
219
|
const parsed = text.split("\n").filter((line) => line.trim() !== "").map((line) => parseJson(line, schema));
|
|
221
220
|
const values = parsed.filter((value) => value !== null);
|
|
222
221
|
return { values, invalidLines: parsed.length - values.length };
|
|
223
222
|
}
|
|
223
|
+
async function readJsonLines(path, schema) {
|
|
224
|
+
const text = await readTextOrNull(path) ?? "";
|
|
225
|
+
return parseJsonLines(text, schema);
|
|
226
|
+
}
|
|
224
227
|
function toJsonLines(values) {
|
|
225
228
|
return values.map((value) => `${JSON.stringify(value)}
|
|
226
229
|
`).join("");
|
|
227
230
|
}
|
|
231
|
+
var NEWLINE = 10;
|
|
232
|
+
async function appendJsonLines(path, values) {
|
|
233
|
+
await withFile(
|
|
234
|
+
path,
|
|
235
|
+
async (handle) => {
|
|
236
|
+
const { size } = await handle.stat();
|
|
237
|
+
const endsMidLine = size > 0 && (await readAt(handle, size - 1, 1))[0] !== NEWLINE;
|
|
238
|
+
await handle.appendFile((endsMidLine ? "\n" : "") + toJsonLines(values), "utf8");
|
|
239
|
+
},
|
|
240
|
+
"a+"
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
async function withFile(path, use, flags = "r") {
|
|
244
|
+
return closingAfter(await open(path, flags), use);
|
|
245
|
+
}
|
|
246
|
+
async function withExistingFile(path, use) {
|
|
247
|
+
const handle = await open(path, "r").catch((error) => {
|
|
248
|
+
if (hasErrorCode(error, "ENOENT")) return null;
|
|
249
|
+
throw error;
|
|
250
|
+
});
|
|
251
|
+
return handle === null ? null : closingAfter(handle, use);
|
|
252
|
+
}
|
|
253
|
+
async function closingAfter(handle, use) {
|
|
254
|
+
try {
|
|
255
|
+
return await use(handle);
|
|
256
|
+
} finally {
|
|
257
|
+
await handle.close();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function readAt(handle, position, length) {
|
|
261
|
+
const buffer = Buffer.alloc(length);
|
|
262
|
+
const { bytesRead } = await handle.read(buffer, 0, length, position);
|
|
263
|
+
return buffer.subarray(0, bytesRead);
|
|
264
|
+
}
|
|
265
|
+
function readFileAt(path, position, length) {
|
|
266
|
+
return withFile(path, (handle) => readAt(handle, position, length));
|
|
267
|
+
}
|
|
228
268
|
function parseJson(text, schema) {
|
|
229
269
|
try {
|
|
230
270
|
const parsed = schema.safeParse(JSON.parse(text));
|
|
@@ -400,10 +440,7 @@ var cliRunSchema = z.object({
|
|
|
400
440
|
async function appendRun(root, run) {
|
|
401
441
|
await mkdir(root, { recursive: true });
|
|
402
442
|
const path = join3(root, RUNS_FILE);
|
|
403
|
-
await withFileLock(path, () =>
|
|
404
|
-
}
|
|
405
|
-
async function readRuns(root) {
|
|
406
|
-
return (await readJsonLines(join3(root, RUNS_FILE), cliRunSchema)).values;
|
|
443
|
+
await withFileLock(path, () => appendJsonLines(path, [run]));
|
|
407
444
|
}
|
|
408
445
|
async function trimRuns(root, now) {
|
|
409
446
|
const path = join3(root, RUNS_FILE);
|
|
@@ -423,20 +460,13 @@ async function trimRunsWhenStale(root, now) {
|
|
|
423
460
|
return needsTrim ? trimRuns(root, now) : 0;
|
|
424
461
|
}
|
|
425
462
|
async function firstRun(path) {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const [firstLine = ""] = buffer.toString("utf8", 0, bytesRead).split("\n");
|
|
434
|
-
if (firstLine.trim() === "") return "none";
|
|
435
|
-
const run = parseJson(firstLine, cliRunSchema);
|
|
436
|
-
return run === null ? "unparsable" : Date.parse(run.at);
|
|
437
|
-
} finally {
|
|
438
|
-
await file.close();
|
|
439
|
-
}
|
|
463
|
+
return await withExistingFile(path, firstRunIn) ?? "none";
|
|
464
|
+
}
|
|
465
|
+
async function firstRunIn(handle) {
|
|
466
|
+
const [firstLine = ""] = (await readAt(handle, 0, FIRST_LINE_BYTES)).toString("utf8").split("\n");
|
|
467
|
+
if (firstLine.trim() === "") return "none";
|
|
468
|
+
const run = parseJson(firstLine, cliRunSchema);
|
|
469
|
+
return run === null ? "unparsable" : Date.parse(run.at);
|
|
440
470
|
}
|
|
441
471
|
|
|
442
472
|
// src/core/store/paths.ts
|
|
@@ -598,7 +628,7 @@ var cliEn = {
|
|
|
598
628
|
taskNotFound: (id) => `Task ${id} not found`,
|
|
599
629
|
fileConflict: (id) => `Task file ${id} changed while writing, retry the command`,
|
|
600
630
|
statsTitle: (scopeName) => `${scopeName} \xB7 stats`,
|
|
601
|
-
statsOpenLine: ({ open:
|
|
631
|
+
statsOpenLine: ({ open: open2, weight, net, created, closed }) => `Open: ${open2} (weight ${weight}) \xB7 this week: ${net} (created ${created}, closed ${closed})`,
|
|
602
632
|
statsAgeLine: (ageMedian, leadMedian, tail) => `Age, median: ${ageMedian} \xB7 time to close, median: ${leadMedian}${tail}`,
|
|
603
633
|
statsP90Tail: (p903) => ` (90% \u2014 ${p903})`,
|
|
604
634
|
statsForecastLine: (forecast3, tail) => `Forecast: ${forecast3} (${tail})`,
|
|
@@ -652,7 +682,7 @@ var cliEn = {
|
|
|
652
682
|
"delete <id> --confirm <id> (deletes the project directory with all its tasks)"
|
|
653
683
|
],
|
|
654
684
|
noProjects: "No projects",
|
|
655
|
-
projectListLine: ({ id, name, prefix, statusWord: statusWord2, open:
|
|
685
|
+
projectListLine: ({ id, name, prefix, statusWord: statusWord2, open: open2 }) => `${id} \xB7 ${name} \xB7 ${prefix} \xB7 ${statusWord2} \xB7 open ${open2}`,
|
|
656
686
|
confirmProjectDelete: (id) => `Confirm the deletion: backlog project delete ${id} --confirm ${id}`,
|
|
657
687
|
projectDeleted: (id, taskCount) => `${id} deleted: tasks ${taskCount}`,
|
|
658
688
|
projectActive: "active",
|
|
@@ -775,7 +805,7 @@ var cliRu = {
|
|
|
775
805
|
taskNotFound: (id) => `\u0417\u0430\u0434\u0430\u0447\u0430 ${id} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430`,
|
|
776
806
|
fileConflict: (id) => `\u0424\u0430\u0439\u043B \u0437\u0430\u0434\u0430\u0447\u0438 ${id} \u0438\u0437\u043C\u0435\u043D\u0438\u043B\u0441\u044F \u0432\u043E \u0432\u0440\u0435\u043C\u044F \u0437\u0430\u043F\u0438\u0441\u0438, \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0435 \u043A\u043E\u043C\u0430\u043D\u0434\u0443`,
|
|
777
807
|
statsTitle: (scopeName) => `${scopeName} \xB7 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043A\u0430`,
|
|
778
|
-
statsOpenLine: ({ open:
|
|
808
|
+
statsOpenLine: ({ open: open2, weight, net, created, closed }) => `\u041E\u0442\u043A\u0440\u044B\u0442\u043E: ${open2} (\u0432\u0435\u0441 ${weight}) \xB7 \u0437\u0430 \u043D\u0435\u0434\u0435\u043B\u044E: ${net} (\u0441\u043E\u0437\u0434\u0430\u043D\u043E ${created}, \u0437\u0430\u043A\u0440\u044B\u0442\u043E ${closed})`,
|
|
779
809
|
statsAgeLine: (ageMedian, leadMedian, tail) => `\u0412\u043E\u0437\u0440\u0430\u0441\u0442, \u043C\u0435\u0434\u0438\u0430\u043D\u0430: ${ageMedian} \xB7 \u0434\u043E \u0437\u0430\u043A\u0440\u044B\u0442\u0438\u044F, \u043C\u0435\u0434\u0438\u0430\u043D\u0430: ${leadMedian}${tail}`,
|
|
780
810
|
statsP90Tail: (p903) => ` (90% \u2014 ${p903})`,
|
|
781
811
|
statsForecastLine: (forecast3, tail) => `\u041F\u0440\u043E\u0433\u043D\u043E\u0437: ${forecast3} (${tail})`,
|
|
@@ -840,8 +870,8 @@ var cliRu = {
|
|
|
840
870
|
name,
|
|
841
871
|
prefix,
|
|
842
872
|
statusWord: statusWord2,
|
|
843
|
-
open:
|
|
844
|
-
}) => `${id} \xB7 ${name} \xB7 ${prefix} \xB7 ${statusWord2} \xB7 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0445 ${
|
|
873
|
+
open: open2
|
|
874
|
+
}) => `${id} \xB7 ${name} \xB7 ${prefix} \xB7 ${statusWord2} \xB7 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0445 ${open2}`,
|
|
845
875
|
confirmProjectDelete: (id) => `\u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u0435: backlog project delete ${id} --confirm ${id}`,
|
|
846
876
|
projectDeleted: (id, taskCount) => `${id} \u0443\u0434\u0430\u043B\u0451\u043D: \u0437\u0430\u0434\u0430\u0447 ${taskCount}`,
|
|
847
877
|
projectActive: "\u0430\u043A\u0442\u0438\u0432\u0435\u043D",
|
|
@@ -1477,8 +1507,8 @@ function p90(value) {
|
|
|
1477
1507
|
if (value === null) return "\u2014";
|
|
1478
1508
|
return value < 1 ? "within a day" : `within ${days(value)}`;
|
|
1479
1509
|
}
|
|
1480
|
-
function forecast({ open:
|
|
1481
|
-
if (
|
|
1510
|
+
function forecast({ open: open2, weeklyNet, weeks, until }) {
|
|
1511
|
+
if (open2 === 0) return "No open tasks";
|
|
1482
1512
|
if (weeks !== null && until !== null) return `Debt clears in about ${weeks} wk. (by ${formatDayMonth("en", new Date(until))})`;
|
|
1483
1513
|
if (weeklyNet === 0) return "Debt is not shrinking";
|
|
1484
1514
|
const growth = roundToTenth(-weeklyNet);
|
|
@@ -1710,8 +1740,8 @@ function p902(value) {
|
|
|
1710
1740
|
if (value === null) return "\u2014";
|
|
1711
1741
|
return value < 1 ? "\u0431\u044B\u0441\u0442\u0440\u0435\u0435 \u0441\u0443\u0442\u043E\u043A" : `\u0437\u0430 ${days2(value)}`;
|
|
1712
1742
|
}
|
|
1713
|
-
function forecast2({ open:
|
|
1714
|
-
if (
|
|
1743
|
+
function forecast2({ open: open2, weeklyNet, weeks, until }) {
|
|
1744
|
+
if (open2 === 0) return "\u041E\u0442\u043A\u0440\u044B\u0442\u044B\u0445 \u0437\u0430\u0434\u0430\u0447 \u043D\u0435\u0442";
|
|
1715
1745
|
if (weeks !== null && until !== null) return `\u0414\u043E\u043B\u0433 \u0440\u0430\u0437\u0431\u0435\u0440\u0451\u0442\u0441\u044F \u043F\u0440\u0438\u043C\u0435\u0440\u043D\u043E \u0437\u0430 ${weeks}${NBSP}\u043D\u0435\u0434. (\u043A ${formatDayMonth("ru", new Date(until))})`;
|
|
1716
1746
|
if (weeklyNet === 0) return "\u0414\u043E\u043B\u0433 \u043D\u0435 \u0443\u043C\u0435\u043D\u044C\u0448\u0430\u0435\u0442\u0441\u044F";
|
|
1717
1747
|
const growth = roundToTenth(-weeklyNet);
|
|
@@ -2149,20 +2179,21 @@ function findBlockerCycle(start, resolve7) {
|
|
|
2149
2179
|
}
|
|
2150
2180
|
|
|
2151
2181
|
// src/core/store/journal.ts
|
|
2152
|
-
import { appendFile as appendFile2 } from "node:fs/promises";
|
|
2153
2182
|
import { join as join7 } from "node:path";
|
|
2154
2183
|
var JOURNAL_FILE = "journal.jsonl";
|
|
2155
2184
|
async function appendJournal(projectDir2, events, onError = reportToStderr) {
|
|
2156
2185
|
if (events.length === 0) return;
|
|
2157
2186
|
const path = join7(projectDir2, JOURNAL_FILE);
|
|
2158
2187
|
try {
|
|
2159
|
-
await
|
|
2188
|
+
await appendJsonLines(path, events);
|
|
2160
2189
|
} catch (error) {
|
|
2161
2190
|
onError(path, error);
|
|
2162
2191
|
}
|
|
2163
2192
|
}
|
|
2164
2193
|
async function readJournal(projectDir2, projectId) {
|
|
2165
|
-
|
|
2194
|
+
return projectJournal(projectId, await readJsonLines(join7(projectDir2, JOURNAL_FILE), journalEventSchema));
|
|
2195
|
+
}
|
|
2196
|
+
function projectJournal(projectId, { values, invalidLines }) {
|
|
2166
2197
|
return { projectId, events: values, invalidLines };
|
|
2167
2198
|
}
|
|
2168
2199
|
async function readJournals(root, projectIds) {
|
|
@@ -2940,11 +2971,11 @@ function anchorState(task, facts) {
|
|
|
2940
2971
|
return moved === null || movedAnchor === null ? { kind: "changed" } : { kind: "moved", source: moved, anchor: movedAnchor };
|
|
2941
2972
|
}
|
|
2942
2973
|
function findSimilarTask(draft, tasks) {
|
|
2943
|
-
const
|
|
2974
|
+
const open2 = tasks.filter((task) => task.type === "task" && !isClosed(task.status));
|
|
2944
2975
|
const { source } = draft;
|
|
2945
|
-
const bySource = source === void 0 ? void 0 :
|
|
2976
|
+
const bySource = source === void 0 ? void 0 : open2.find((task) => task.source !== void 0 && samePlace(task.source, source));
|
|
2946
2977
|
if (bySource !== void 0) return { task: taskRef(bySource), match: "source" };
|
|
2947
|
-
const byTitle =
|
|
2978
|
+
const byTitle = open2.find((task) => similarTitles(task.title, draft.title));
|
|
2948
2979
|
return byTitle === void 0 ? null : { task: taskRef(byTitle), match: "title" };
|
|
2949
2980
|
}
|
|
2950
2981
|
function duplicateCandidates(tasks, symbolOf = () => null) {
|
|
@@ -4640,12 +4671,12 @@ async function rememberShown(projectDir2, shown, io) {
|
|
|
4640
4671
|
|
|
4641
4672
|
// src/cli/describe.ts
|
|
4642
4673
|
function describeTask(task, index) {
|
|
4643
|
-
const
|
|
4674
|
+
const open2 = openBlockers(task, index);
|
|
4644
4675
|
return {
|
|
4645
4676
|
task,
|
|
4646
4677
|
progress: taskProgress(task, index),
|
|
4647
|
-
openBlockers:
|
|
4648
|
-
inactiveBlockerIds: task.blockedBy.filter((id) => !
|
|
4678
|
+
openBlockers: open2,
|
|
4679
|
+
inactiveBlockerIds: task.blockedBy.filter((id) => !open2.some((blocker) => blocker.id === id)),
|
|
4649
4680
|
blocks: dependentTasks(task, index),
|
|
4650
4681
|
related: relatedTasks(task, index),
|
|
4651
4682
|
epic: task.epic === void 0 ? void 0 : index.byId.get(task.epic),
|
|
@@ -4830,8 +4861,8 @@ async function listProjects(io) {
|
|
|
4830
4861
|
}
|
|
4831
4862
|
const index = buildIndex(loaded.tasks);
|
|
4832
4863
|
for (const project of loaded.projects) {
|
|
4833
|
-
const
|
|
4834
|
-
io.print(cli.projectListLine({ id: project.id, name: project.name, prefix: project.prefix, statusWord: statusWord(cli, project.active), open:
|
|
4864
|
+
const open2 = filterTasks(loaded.tasks, { projectId: project.id, statuses: OPEN_STATUSES }, index).length;
|
|
4865
|
+
io.print(cli.projectListLine({ id: project.id, name: project.name, prefix: project.prefix, statusWord: statusWord(cli, project.active), open: open2 }));
|
|
4835
4866
|
}
|
|
4836
4867
|
return EXIT.ok;
|
|
4837
4868
|
}
|
|
@@ -5012,7 +5043,7 @@ function listenFailure(error, port, messages) {
|
|
|
5012
5043
|
// src/server/start.ts
|
|
5013
5044
|
import { serve } from "@hono/node-server";
|
|
5014
5045
|
import { mkdir as mkdir6, rm as rm4, writeFile as writeFile3 } from "node:fs/promises";
|
|
5015
|
-
import { join as
|
|
5046
|
+
import { join as join28 } from "node:path";
|
|
5016
5047
|
|
|
5017
5048
|
// src/core/store/sweep.ts
|
|
5018
5049
|
import { dirname as dirname7, join as join22 } from "node:path";
|
|
@@ -5162,7 +5193,7 @@ async function reserveNumbers(projects, expired) {
|
|
|
5162
5193
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
5163
5194
|
import { Hono as Hono3 } from "hono";
|
|
5164
5195
|
import { secureHeaders } from "hono/secure-headers";
|
|
5165
|
-
import { join as
|
|
5196
|
+
import { join as join25 } from "node:path";
|
|
5166
5197
|
|
|
5167
5198
|
// src/server/api.ts
|
|
5168
5199
|
import { Hono as Hono2 } from "hono";
|
|
@@ -5292,17 +5323,22 @@ function previousOf(task) {
|
|
|
5292
5323
|
function createReportCache({ ttlMs, now }) {
|
|
5293
5324
|
let entries = /* @__PURE__ */ new Map();
|
|
5294
5325
|
return {
|
|
5295
|
-
get: (key, compute) => {
|
|
5326
|
+
get: (key, compute, tags = []) => {
|
|
5296
5327
|
const cached = entries.get(key);
|
|
5297
5328
|
if (cached !== void 0 && now() - cached.at <= ttlMs) return cached.value;
|
|
5298
|
-
const generation = entries;
|
|
5299
5329
|
const value = compute();
|
|
5300
|
-
|
|
5301
|
-
value.catch(() =>
|
|
5330
|
+
entries.set(key, { at: now(), value, tags });
|
|
5331
|
+
value.catch(() => {
|
|
5332
|
+
if (entries.get(key)?.value === value) entries.delete(key);
|
|
5333
|
+
});
|
|
5302
5334
|
return value;
|
|
5303
5335
|
},
|
|
5304
5336
|
clear: () => {
|
|
5305
5337
|
entries = /* @__PURE__ */ new Map();
|
|
5338
|
+
},
|
|
5339
|
+
clearTagged: (tags) => {
|
|
5340
|
+
const tagged = new Set(tags);
|
|
5341
|
+
for (const [key, entry] of entries) if (entry.tags.some((tag) => tagged.has(tag))) entries.delete(key);
|
|
5306
5342
|
}
|
|
5307
5343
|
};
|
|
5308
5344
|
}
|
|
@@ -5347,32 +5383,36 @@ async function markOf(path) {
|
|
|
5347
5383
|
|
|
5348
5384
|
// src/server/stats-api.ts
|
|
5349
5385
|
import { Hono } from "hono";
|
|
5386
|
+
import { relative as relative3, sep as sep4 } from "node:path";
|
|
5350
5387
|
|
|
5351
5388
|
// src/core/code/code-cache.ts
|
|
5352
5389
|
import { join as join23 } from "node:path";
|
|
5353
5390
|
import { z as z15 } from "zod";
|
|
5354
5391
|
var CODE_CACHE_FILE = ".code-cache.json";
|
|
5355
|
-
var CODE_CACHE_VERSION =
|
|
5356
|
-
var
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5392
|
+
var CODE_CACHE_VERSION = 2;
|
|
5393
|
+
var repoScanSchema = z15.object({
|
|
5394
|
+
head: z15.string(),
|
|
5395
|
+
main: z15.string().nullable(),
|
|
5396
|
+
commits: z15.array(z15.object({ date: z15.string(), paths: z15.array(z15.string()) })),
|
|
5397
|
+
units: z15.array(z15.object({ date: z15.string(), lines: z15.number() })),
|
|
5398
|
+
lines: z15.array(z15.object({ path: z15.string(), lines: z15.number() }))
|
|
5360
5399
|
});
|
|
5361
5400
|
var fixCommitSchema = z15.object({ date: z15.string(), landedAt: z15.string().optional(), byAgent: z15.boolean(), lines: z15.number(), testLines: z15.number() });
|
|
5362
5401
|
var snapshotSchema = z15.object({
|
|
5363
5402
|
version: z15.literal(CODE_CACHE_VERSION),
|
|
5364
|
-
repos: z15.record(z15.string(),
|
|
5365
|
-
fixes: z15.record(z15.string(), fixCommitSchema)
|
|
5403
|
+
repos: z15.record(z15.string(), repoScanSchema),
|
|
5404
|
+
fixes: z15.record(z15.string(), fixCommitSchema),
|
|
5405
|
+
unsettled: z15.record(z15.string(), z15.string().nullable())
|
|
5366
5406
|
});
|
|
5367
5407
|
function emptyCodeCache() {
|
|
5368
|
-
return { repos: {}, fixes: {} };
|
|
5408
|
+
return { repos: {}, fixes: {}, unsettled: {} };
|
|
5369
5409
|
}
|
|
5370
5410
|
function createCodeCacheFile(root) {
|
|
5371
5411
|
const path = join23(root, CODE_CACHE_FILE);
|
|
5372
5412
|
return {
|
|
5373
5413
|
read: async () => {
|
|
5374
5414
|
const snapshot = await readJsonFile(path, snapshotSchema);
|
|
5375
|
-
return snapshot === null ? emptyCodeCache() : { repos: snapshot.repos, fixes: snapshot.fixes };
|
|
5415
|
+
return snapshot === null ? emptyCodeCache() : { repos: snapshot.repos, fixes: snapshot.fixes, unsettled: snapshot.unsettled };
|
|
5376
5416
|
},
|
|
5377
5417
|
write: (snapshot) => writeFileAtomic(path, JSON.stringify({ version: CODE_CACHE_VERSION, ...snapshot }))
|
|
5378
5418
|
};
|
|
@@ -5380,6 +5420,9 @@ function createCodeCacheFile(root) {
|
|
|
5380
5420
|
|
|
5381
5421
|
// src/core/code/code-window.ts
|
|
5382
5422
|
var CHURN_DAYS = 90;
|
|
5423
|
+
function churnWindowStart(now) {
|
|
5424
|
+
return new Date(now.getTime() - CHURN_DAYS * DAY_MS);
|
|
5425
|
+
}
|
|
5383
5426
|
|
|
5384
5427
|
// src/core/code/test-paths.ts
|
|
5385
5428
|
var TEST_DIRECTORIES = /* @__PURE__ */ new Set(["test", "tests", "__tests__", "e2e", "spec"]);
|
|
@@ -5401,7 +5444,6 @@ var CHURN_EXCLUDES = [...LOCK_EXCLUDES, ...NON_CODE_EXTENSIONS.map((ext) => `:!*
|
|
|
5401
5444
|
var HEAD = "HEAD";
|
|
5402
5445
|
var MAIN_REFS = ["origin/HEAD", "main", "master"];
|
|
5403
5446
|
var AGENT_TRAILER = /^claude/i;
|
|
5404
|
-
var GREP_PREFIX = "HEAD:";
|
|
5405
5447
|
var RENAME_ARROW = " => ";
|
|
5406
5448
|
async function readRefs(git, repo) {
|
|
5407
5449
|
const commits = await resolveCommits(git, repo, [HEAD, ...MAIN_REFS]) ?? /* @__PURE__ */ new Map();
|
|
@@ -5409,14 +5451,68 @@ async function readRefs(git, repo) {
|
|
|
5409
5451
|
const main = MAIN_REFS.map((ref) => commits.get(ref)).find((commit) => commit !== void 0);
|
|
5410
5452
|
return { head, main: main ?? head };
|
|
5411
5453
|
}
|
|
5412
|
-
async function
|
|
5413
|
-
const
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5454
|
+
async function readCommitsSince(git, repo, since, range) {
|
|
5455
|
+
const output = await git(repo, [
|
|
5456
|
+
"log",
|
|
5457
|
+
revisionsOf(range),
|
|
5458
|
+
"--full-history",
|
|
5459
|
+
"--sparse",
|
|
5460
|
+
`--since=${since.toISOString()}`,
|
|
5461
|
+
`--format=tformat:${RECORD}%P${FIELD}%cI`,
|
|
5462
|
+
"--name-only",
|
|
5463
|
+
"-M",
|
|
5464
|
+
"--relative",
|
|
5465
|
+
"-z",
|
|
5466
|
+
"--",
|
|
5467
|
+
"."
|
|
5417
5468
|
]);
|
|
5418
|
-
if (
|
|
5419
|
-
|
|
5469
|
+
if (output === null) return null;
|
|
5470
|
+
const { after } = range;
|
|
5471
|
+
const commits = records(output).map((record) => {
|
|
5472
|
+
const [header = "", ...files] = record.split("\0");
|
|
5473
|
+
return { ...parseHistoryHeader(header), paths: files.map((file) => file.replace(/^\n/, "")).filter((file) => file !== "") };
|
|
5474
|
+
});
|
|
5475
|
+
return {
|
|
5476
|
+
entries: commits.filter(({ paths }) => paths.length > 0).map(({ date, paths }) => ({ date, paths })),
|
|
5477
|
+
reachesAfter: after === void 0 || commits.some(({ parents }) => parents.includes(after))
|
|
5478
|
+
};
|
|
5479
|
+
}
|
|
5480
|
+
async function readUnits(git, repo, since, range) {
|
|
5481
|
+
const output = await git(repo, [
|
|
5482
|
+
"log",
|
|
5483
|
+
revisionsOf(range),
|
|
5484
|
+
"--first-parent",
|
|
5485
|
+
"--sparse",
|
|
5486
|
+
"--diff-merges=first-parent",
|
|
5487
|
+
`--since=${since.toISOString()}`,
|
|
5488
|
+
`--format=tformat:${RECORD}%P${FIELD}%cI`,
|
|
5489
|
+
"--numstat",
|
|
5490
|
+
"--relative",
|
|
5491
|
+
"--",
|
|
5492
|
+
".",
|
|
5493
|
+
...CHURN_EXCLUDES
|
|
5494
|
+
]);
|
|
5495
|
+
if (output === null) return null;
|
|
5496
|
+
const { after } = range;
|
|
5497
|
+
const units = records(output).map((record) => {
|
|
5498
|
+
const [header = "", ...rows] = record.split("\n");
|
|
5499
|
+
return { ...parseHistoryHeader(header), rows: rows.filter((row) => row.trim() !== "") };
|
|
5500
|
+
});
|
|
5501
|
+
return {
|
|
5502
|
+
entries: units.filter(({ rows }) => rows.length > 0).map(({ date, rows }) => ({ date, lines: numstatLines(rows) })),
|
|
5503
|
+
reachesAfter: after === void 0 || units.some(({ parents }) => parents[0] === after)
|
|
5504
|
+
};
|
|
5505
|
+
}
|
|
5506
|
+
async function readLines(git, repo, head) {
|
|
5507
|
+
const output = await git(repo, ["grep", "-I", "-c", "-z", "", head, "--", ".", ...LOCK_EXCLUDES]);
|
|
5508
|
+
return output === null ? null : parseLines(output, `${head}:`);
|
|
5509
|
+
}
|
|
5510
|
+
function revisionsOf({ tip, after }) {
|
|
5511
|
+
return after === void 0 ? tip : `${after}..${tip}`;
|
|
5512
|
+
}
|
|
5513
|
+
function parseHistoryHeader(header) {
|
|
5514
|
+
const [parents = "", date = ""] = header.split(FIELD);
|
|
5515
|
+
return { parents: parents.split(" ").filter((parent) => parent !== ""), date: date.trim() };
|
|
5420
5516
|
}
|
|
5421
5517
|
async function readFixCommits(git, repo, { hashes, mainCommit }) {
|
|
5422
5518
|
const fullHashes = await resolveCommits(git, repo, hashes);
|
|
@@ -5480,29 +5576,6 @@ function parseFixStats(output) {
|
|
|
5480
5576
|
function records(output) {
|
|
5481
5577
|
return output.split(RECORD).filter((record) => record.trim() !== "");
|
|
5482
5578
|
}
|
|
5483
|
-
async function readUnits(git, repo, since, mainCommit) {
|
|
5484
|
-
if (mainCommit === null) return [];
|
|
5485
|
-
const output = await git(repo, [
|
|
5486
|
-
"log",
|
|
5487
|
-
mainCommit,
|
|
5488
|
-
"--first-parent",
|
|
5489
|
-
"--diff-merges=first-parent",
|
|
5490
|
-
`--since=${since.toISOString()}`,
|
|
5491
|
-
`--format=tformat:${RECORD}%cI`,
|
|
5492
|
-
"--numstat",
|
|
5493
|
-
"--relative",
|
|
5494
|
-
"--",
|
|
5495
|
-
".",
|
|
5496
|
-
...CHURN_EXCLUDES
|
|
5497
|
-
]);
|
|
5498
|
-
return output === null ? [] : parseUnits(output);
|
|
5499
|
-
}
|
|
5500
|
-
function parseUnits(output) {
|
|
5501
|
-
return output.split(RECORD).filter((record) => record.trim() !== "").map((record) => {
|
|
5502
|
-
const [date = "", ...rows] = record.split("\n");
|
|
5503
|
-
return { date: date.trim(), lines: numstatLines(rows) };
|
|
5504
|
-
});
|
|
5505
|
-
}
|
|
5506
5579
|
function numstatLines(rows) {
|
|
5507
5580
|
return sum(rows.filter((row) => row.trim() !== "").map(numstatRowLines).filter((lines) => !Number.isNaN(lines)));
|
|
5508
5581
|
}
|
|
@@ -5510,43 +5583,95 @@ function numstatRowLines(row) {
|
|
|
5510
5583
|
const [added = "", deleted = ""] = row.split(" ");
|
|
5511
5584
|
return Number(added) + Number(deleted);
|
|
5512
5585
|
}
|
|
5513
|
-
function
|
|
5514
|
-
return output.split(
|
|
5515
|
-
|
|
5516
|
-
function parseLines(output) {
|
|
5517
|
-
return output.split("\n").filter((record) => record.startsWith(GREP_PREFIX)).map((record) => {
|
|
5518
|
-
const [path = "", count3 = ""] = record.slice(GREP_PREFIX.length).split("\0");
|
|
5586
|
+
function parseLines(output, prefix) {
|
|
5587
|
+
return output.split("\n").filter((record) => record.startsWith(prefix)).map((record) => {
|
|
5588
|
+
const [path = "", count3 = ""] = record.slice(prefix.length).split("\0");
|
|
5519
5589
|
return { path, lines: Number(count3) };
|
|
5520
5590
|
});
|
|
5521
5591
|
}
|
|
5522
5592
|
|
|
5593
|
+
// src/core/code/repo-scan.ts
|
|
5594
|
+
async function scanRepo(git, repo, { refs, now, previous }) {
|
|
5595
|
+
const { head, main } = refs;
|
|
5596
|
+
if (head === null) return null;
|
|
5597
|
+
const since = churnWindowStart(now);
|
|
5598
|
+
const [commits, units, lines] = await Promise.all([
|
|
5599
|
+
extendHistory((range) => readCommitsSince(git, repo, since, range), head, historyOf2(previous?.head, previous?.commits)),
|
|
5600
|
+
main === null ? [] : extendHistory((range) => readUnits(git, repo, since, range), main, historyOf2(previous?.main, previous?.units)),
|
|
5601
|
+
previous?.head === head ? previous.lines : readLines(git, repo, head)
|
|
5602
|
+
]);
|
|
5603
|
+
if (commits === null || units === null || lines === null) return null;
|
|
5604
|
+
const scan = { head, main, commits: windowOf2(commits, since), units: windowOf2(units, since), lines };
|
|
5605
|
+
return previous !== void 0 && isSameScan(previous, scan) ? previous : scan;
|
|
5606
|
+
}
|
|
5607
|
+
function repoCodeOf(scan) {
|
|
5608
|
+
return { commits: scan.commits.map(({ paths }) => paths), lines: scan.lines, units: scan.units };
|
|
5609
|
+
}
|
|
5610
|
+
function historyOf2(tip, entries) {
|
|
5611
|
+
return tip === void 0 || tip === null || entries === void 0 ? void 0 : { tip, entries };
|
|
5612
|
+
}
|
|
5613
|
+
async function extendHistory(read, tip, previous) {
|
|
5614
|
+
if (previous?.tip === tip) return previous.entries;
|
|
5615
|
+
if (previous !== void 0) {
|
|
5616
|
+
const added = await read({ tip, after: previous.tip });
|
|
5617
|
+
if (added?.reachesAfter === true) return [...added.entries, ...previous.entries];
|
|
5618
|
+
}
|
|
5619
|
+
return (await read({ tip }))?.entries ?? null;
|
|
5620
|
+
}
|
|
5621
|
+
function windowOf2(entries, since) {
|
|
5622
|
+
return entries.filter((entry) => Date.parse(entry.date) >= since.getTime()).sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
|
|
5623
|
+
}
|
|
5624
|
+
function isSameScan(previous, scan) {
|
|
5625
|
+
return previous.head === scan.head && previous.main === scan.main && previous.commits.length === scan.commits.length && previous.units.length === scan.units.length;
|
|
5626
|
+
}
|
|
5627
|
+
|
|
5523
5628
|
// src/core/code/code-source.ts
|
|
5524
5629
|
function createCodeSource({ home: home2, git = runGit, store, onError = () => {
|
|
5525
5630
|
} }) {
|
|
5526
5631
|
const repoCache = /* @__PURE__ */ new Map();
|
|
5527
5632
|
const fixCache = /* @__PURE__ */ new Map();
|
|
5633
|
+
const unsettledCheckedAt = /* @__PURE__ */ new Map();
|
|
5528
5634
|
let changed = false;
|
|
5529
5635
|
let restored = null;
|
|
5636
|
+
let retainedRepos = null;
|
|
5530
5637
|
const restore = () => {
|
|
5531
5638
|
restored ??= readSnapshot(store, (error) => onError("read", error)).then((snapshot) => {
|
|
5532
5639
|
for (const [repo, entry] of Object.entries(snapshot.repos)) if (!repoCache.has(repo)) repoCache.set(repo, entry);
|
|
5533
5640
|
for (const [key, commit] of Object.entries(snapshot.fixes)) if (!fixCache.has(key)) fixCache.set(key, commit);
|
|
5641
|
+
for (const [key, main] of Object.entries(snapshot.unsettled)) if (!unsettledCheckedAt.has(key)) unsettledCheckedAt.set(key, main);
|
|
5534
5642
|
});
|
|
5535
5643
|
return restored;
|
|
5536
5644
|
};
|
|
5537
5645
|
const dropStaleFixes = (now, requested) => {
|
|
5538
|
-
const oldest = now.getTime()
|
|
5646
|
+
const oldest = churnWindowStart(now).getTime();
|
|
5539
5647
|
for (const [key, commit] of fixCache) {
|
|
5540
5648
|
if (requested.has(key) || Date.parse(commit.date) >= oldest) continue;
|
|
5541
5649
|
fixCache.delete(key);
|
|
5650
|
+
unsettledCheckedAt.delete(key);
|
|
5651
|
+
changed = true;
|
|
5652
|
+
}
|
|
5653
|
+
};
|
|
5654
|
+
const dropUnretainedRepos = () => {
|
|
5655
|
+
if (retainedRepos === null) return;
|
|
5656
|
+
const kept = retainedRepos;
|
|
5657
|
+
for (const repo of repoCache.keys()) {
|
|
5658
|
+
if (kept.has(repo)) continue;
|
|
5659
|
+
repoCache.delete(repo);
|
|
5660
|
+
changed = true;
|
|
5661
|
+
}
|
|
5662
|
+
for (const key of /* @__PURE__ */ new Set([...fixCache.keys(), ...unsettledCheckedAt.keys()])) {
|
|
5663
|
+
if (kept.has(repoOfFixCacheKey(key))) continue;
|
|
5664
|
+
fixCache.delete(key);
|
|
5665
|
+
unsettledCheckedAt.delete(key);
|
|
5542
5666
|
changed = true;
|
|
5543
5667
|
}
|
|
5544
5668
|
};
|
|
5545
5669
|
let writing = Promise.resolve();
|
|
5546
5670
|
const persist = () => {
|
|
5671
|
+
dropUnretainedRepos();
|
|
5547
5672
|
if (store === void 0 || !changed) return writing;
|
|
5548
5673
|
changed = false;
|
|
5549
|
-
const snapshot = { repos: Object.fromEntries(repoCache), fixes: Object.fromEntries(fixCache) };
|
|
5674
|
+
const snapshot = { repos: Object.fromEntries(repoCache), fixes: Object.fromEntries(fixCache), unsettled: Object.fromEntries(unsettledCheckedAt) };
|
|
5550
5675
|
writing = writing.then(
|
|
5551
5676
|
() => store.write(snapshot).catch((error) => {
|
|
5552
5677
|
changed = true;
|
|
@@ -5564,35 +5689,39 @@ function createCodeSource({ home: home2, git = runGit, store, onError = () => {
|
|
|
5564
5689
|
return started;
|
|
5565
5690
|
};
|
|
5566
5691
|
const readRepoOnce = async (repo, now) => {
|
|
5567
|
-
const
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5692
|
+
const previous = repoCache.get(repo);
|
|
5693
|
+
const scan = await scanRepo(git, repo, { refs: await readRefs(git, repo), now, previous });
|
|
5694
|
+
if (scan === null) return null;
|
|
5695
|
+
if (scan !== previous) {
|
|
5696
|
+
repoCache.set(repo, scan);
|
|
5697
|
+
changed = true;
|
|
5698
|
+
}
|
|
5699
|
+
return repoCodeOf(scan);
|
|
5700
|
+
};
|
|
5701
|
+
const rememberUnsettled = (key, main, commit) => {
|
|
5702
|
+
const checkedAt = commit.landedAt === void 0 ? main : void 0;
|
|
5703
|
+
if (unsettledCheckedAt.get(key) === checkedAt) return;
|
|
5704
|
+
if (checkedAt === void 0) unsettledCheckedAt.delete(key);
|
|
5705
|
+
else unsettledCheckedAt.set(key, checkedAt);
|
|
5575
5706
|
changed = true;
|
|
5576
|
-
return { key, code };
|
|
5577
5707
|
};
|
|
5578
|
-
const unsettledCheckedAt = /* @__PURE__ */ new Map();
|
|
5579
5708
|
const needsReading = (key, main) => {
|
|
5580
5709
|
const cached = fixCache.get(key);
|
|
5581
5710
|
return cached === void 0 || cached.landedAt === void 0 && unsettledCheckedAt.get(key) !== main;
|
|
5582
5711
|
};
|
|
5583
5712
|
const fixCommitsOf = async (repo, main, hashes) => {
|
|
5584
|
-
const unsettled = hashes.filter((hash) => needsReading(
|
|
5713
|
+
const unsettled = hashes.filter((hash) => needsReading(fixCacheKey(repo, hash), main));
|
|
5585
5714
|
if (unsettled.length === 0) return;
|
|
5586
5715
|
const found = await readFixCommits(git, repo, { hashes: unsettled, mainCommit: main });
|
|
5587
5716
|
for (const [hash, commit] of found ?? []) {
|
|
5588
|
-
const key =
|
|
5589
|
-
|
|
5717
|
+
const key = fixCacheKey(repo, hash);
|
|
5718
|
+
rememberUnsettled(key, main, commit);
|
|
5590
5719
|
if (fixCache.has(key) && commit.landedAt === void 0) continue;
|
|
5591
5720
|
fixCache.set(key, commit);
|
|
5592
5721
|
changed = true;
|
|
5593
5722
|
}
|
|
5594
5723
|
};
|
|
5595
|
-
const fixReposOf = async (projects
|
|
5724
|
+
const fixReposOf = async (projects) => {
|
|
5596
5725
|
const refsOf = /* @__PURE__ */ new Map();
|
|
5597
5726
|
const refsOnce = (repo) => remembered(refsOf, repo, () => readRefs(git, repo));
|
|
5598
5727
|
const entries = await Promise.all(
|
|
@@ -5601,7 +5730,7 @@ function createCodeSource({ home: home2, git = runGit, store, onError = () => {
|
|
|
5601
5730
|
project.repos.map(async (repo) => {
|
|
5602
5731
|
const expanded = expandHome(repo, home2);
|
|
5603
5732
|
const refs = await refsOnce(expanded);
|
|
5604
|
-
return refs.head === null ? [] : [{ repo: expanded,
|
|
5733
|
+
return refs.head === null ? [] : [{ repo: expanded, main: refs.main }];
|
|
5605
5734
|
})
|
|
5606
5735
|
);
|
|
5607
5736
|
return [project.id, repos.flat()];
|
|
@@ -5610,6 +5739,9 @@ function createCodeSource({ home: home2, git = runGit, store, onError = () => {
|
|
|
5610
5739
|
return new Map(entries);
|
|
5611
5740
|
};
|
|
5612
5741
|
return {
|
|
5742
|
+
retain: (backlogProjects) => {
|
|
5743
|
+
retainedRepos = new Set(backlogProjects.flatMap((project) => project.repos.map((repo) => expandHome(repo, home2))));
|
|
5744
|
+
},
|
|
5613
5745
|
stateKey: async (projects) => {
|
|
5614
5746
|
const repos = [...new Set(projects.flatMap((project) => project.repos.map((repo) => expandHome(repo, home2))))];
|
|
5615
5747
|
const states = await Promise.all(repos.map(async (repo) => `${repo}@${refsKey(await readRefs(git, repo))}`));
|
|
@@ -5630,7 +5762,7 @@ function createCodeSource({ home: home2, git = runGit, store, onError = () => {
|
|
|
5630
5762
|
const readable = [];
|
|
5631
5763
|
for (const { repo, read } of repos) {
|
|
5632
5764
|
if (read !== null) {
|
|
5633
|
-
readable.push(read
|
|
5765
|
+
readable.push(read);
|
|
5634
5766
|
} else if (!seenUnavailable.has(repo)) {
|
|
5635
5767
|
seenUnavailable.add(repo);
|
|
5636
5768
|
unavailableRepos.push(repo);
|
|
@@ -5643,13 +5775,13 @@ function createCodeSource({ home: home2, git = runGit, store, onError = () => {
|
|
|
5643
5775
|
},
|
|
5644
5776
|
fixCommits: async (projects, requests, now) => {
|
|
5645
5777
|
await restore();
|
|
5646
|
-
const reposOf = await fixReposOf(projects
|
|
5778
|
+
const reposOf = await fixReposOf(projects);
|
|
5647
5779
|
await Promise.all(requests.flatMap(({ projectId, hashes }) => (reposOf.get(projectId) ?? []).map(({ repo, main }) => fixCommitsOf(repo, main, hashes))));
|
|
5648
5780
|
const found = /* @__PURE__ */ new Map();
|
|
5649
5781
|
const requested = /* @__PURE__ */ new Set();
|
|
5650
5782
|
for (const { projectId, hashes } of requests) {
|
|
5651
5783
|
for (const hash of hashes) {
|
|
5652
|
-
const keys = (reposOf.get(projectId) ?? []).map(({ repo }) =>
|
|
5784
|
+
const keys = (reposOf.get(projectId) ?? []).map(({ repo }) => fixCacheKey(repo, hash));
|
|
5653
5785
|
for (const key of keys) requested.add(key);
|
|
5654
5786
|
const commit = keys.map((key) => fixCache.get(key)).find((cached) => cached !== void 0);
|
|
5655
5787
|
if (commit !== void 0) found.set(fixKey(projectId, hash), commit);
|
|
@@ -5661,12 +5793,15 @@ function createCodeSource({ home: home2, git = runGit, store, onError = () => {
|
|
|
5661
5793
|
}
|
|
5662
5794
|
};
|
|
5663
5795
|
}
|
|
5796
|
+
function fixCacheKey(repo, hash) {
|
|
5797
|
+
return `${repo} ${hash}`;
|
|
5798
|
+
}
|
|
5799
|
+
function repoOfFixCacheKey(key) {
|
|
5800
|
+
return key.slice(0, key.lastIndexOf(" "));
|
|
5801
|
+
}
|
|
5664
5802
|
function refsKey(refs) {
|
|
5665
5803
|
return `${refs.head ?? ""} ${refs.main ?? ""}`;
|
|
5666
5804
|
}
|
|
5667
|
-
function repoKey(refs, now) {
|
|
5668
|
-
return `${refsKey(refs)} ${formatLocalDay(now)}`;
|
|
5669
|
-
}
|
|
5670
5805
|
async function readSnapshot(store, onError) {
|
|
5671
5806
|
try {
|
|
5672
5807
|
return await store?.read() ?? emptyCodeCache();
|
|
@@ -5735,14 +5870,14 @@ function folderRows(project, openTasks) {
|
|
|
5735
5870
|
(file) => file.lines
|
|
5736
5871
|
);
|
|
5737
5872
|
const ownSources = openTasks.flatMap((task) => task.projectId === project.projectId && task.source !== void 0 ? [task.source] : []);
|
|
5738
|
-
const
|
|
5739
|
-
return [...
|
|
5873
|
+
const open2 = countBy(ownSources, folderOf);
|
|
5874
|
+
return [...open2.entries()].flatMap(([label, count3]) => {
|
|
5740
5875
|
const folderLines = lines.get(label) ?? 0;
|
|
5741
5876
|
return folderLines >= MIN_FOLDER_LINES ? [{ label, ...densityRow(folderLines, count3) }] : [];
|
|
5742
5877
|
}).sort((a, b) => byDensity(a, b) || a.label.localeCompare(b.label)).slice(0, FOLDER_LIMIT);
|
|
5743
5878
|
}
|
|
5744
|
-
function densityRow(lines,
|
|
5745
|
-
return { lines, open:
|
|
5879
|
+
function densityRow(lines, open2) {
|
|
5880
|
+
return { lines, open: open2, perKloc: lines === 0 ? null : open2 * LINES_PER_UNIT / lines };
|
|
5746
5881
|
}
|
|
5747
5882
|
function byDensity(a, b) {
|
|
5748
5883
|
return (b.perKloc ?? -1) - (a.perKloc ?? -1);
|
|
@@ -6040,8 +6175,8 @@ function sizeOf(samples) {
|
|
|
6040
6175
|
}
|
|
6041
6176
|
function totalsOf2(deferred, units, estimate) {
|
|
6042
6177
|
const fixed = deferred.flatMap((item) => item.fixedLines === null ? [] : [item.fixedLines]);
|
|
6043
|
-
const
|
|
6044
|
-
const estimated = estimatedSizeOf(
|
|
6178
|
+
const open2 = deferred.filter((item) => item.fixedLines === null);
|
|
6179
|
+
const estimated = estimatedSizeOf(open2, estimate);
|
|
6045
6180
|
const estimatedLines = estimated?.lines ?? null;
|
|
6046
6181
|
const realLines = sum(units.map((unit) => unit.lines));
|
|
6047
6182
|
const fixedLines = sum(fixed);
|
|
@@ -6051,15 +6186,15 @@ function totalsOf2(deferred, units, estimate) {
|
|
|
6051
6186
|
realLines,
|
|
6052
6187
|
fixedTasks: fixed.length,
|
|
6053
6188
|
fixedLines,
|
|
6054
|
-
openTasks:
|
|
6189
|
+
openTasks: open2.length,
|
|
6055
6190
|
estimatedLines,
|
|
6056
6191
|
deferredLines,
|
|
6057
6192
|
deferredTestLines: sum(deferred.map((item) => item.fixedTestLines)) + (estimated?.testLines ?? 0),
|
|
6058
|
-
noiseShare: realLines === 0 || estimatedLines === null &&
|
|
6193
|
+
noiseShare: realLines === 0 || estimatedLines === null && open2.length > 0 ? null : Math.min(1, deferredLines / denominator)
|
|
6059
6194
|
};
|
|
6060
6195
|
}
|
|
6061
|
-
function estimatedSizeOf(
|
|
6062
|
-
const estimates =
|
|
6196
|
+
function estimatedSizeOf(open2, estimate) {
|
|
6197
|
+
const estimates = open2.map((item) => estimate(item.history.category));
|
|
6063
6198
|
if (estimates.some((size) => size === null)) return null;
|
|
6064
6199
|
const sizes = estimates.filter((size) => size !== null);
|
|
6065
6200
|
return { lines: sum(sizes.map((size) => size.lines)), testLines: sum(sizes.map((size) => size.testLines)) };
|
|
@@ -6234,9 +6369,150 @@ function previousTotals(histories, nowMs, journalStart) {
|
|
|
6234
6369
|
};
|
|
6235
6370
|
}
|
|
6236
6371
|
|
|
6372
|
+
// src/server/stats-sources.ts
|
|
6373
|
+
import { join as join24 } from "node:path";
|
|
6374
|
+
|
|
6375
|
+
// src/core/store/jsonl-tail.ts
|
|
6376
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
6377
|
+
var HEAD_FINGERPRINT_BYTES = 4096;
|
|
6378
|
+
var EMPTY_FINGERPRINT = createHash5("sha1").digest("hex");
|
|
6379
|
+
function createJsonlTail(path, schema) {
|
|
6380
|
+
let state = emptyState();
|
|
6381
|
+
let generation = 0;
|
|
6382
|
+
let queue = Promise.resolve();
|
|
6383
|
+
function read() {
|
|
6384
|
+
const result = queue.then(doRead);
|
|
6385
|
+
queue = result.catch(() => void 0);
|
|
6386
|
+
return result;
|
|
6387
|
+
}
|
|
6388
|
+
async function doRead() {
|
|
6389
|
+
const pending = await withExistingFile(path, consumeNew);
|
|
6390
|
+
if (pending === null) restart();
|
|
6391
|
+
const { values, invalidLines, bytes } = pending ?? nothingPending();
|
|
6392
|
+
return { values: values.length === 0 ? state.values : [...state.values, ...values], invalidLines: state.invalidLines + invalidLines, length: state.offset + bytes, generation };
|
|
6393
|
+
}
|
|
6394
|
+
async function consumeNew(handle) {
|
|
6395
|
+
const { size } = await handle.stat();
|
|
6396
|
+
if (size < state.offset || !await headMatches(handle)) restart();
|
|
6397
|
+
return size > state.offset ? consumeUpTo(handle, size) : nothingPending();
|
|
6398
|
+
}
|
|
6399
|
+
function restart() {
|
|
6400
|
+
if (state.offset === 0) return;
|
|
6401
|
+
generation += 1;
|
|
6402
|
+
state = emptyState();
|
|
6403
|
+
}
|
|
6404
|
+
async function headMatches(handle) {
|
|
6405
|
+
if (state.offset === 0) return true;
|
|
6406
|
+
return await headFingerprintAt(handle, state.offset) === state.headFingerprint;
|
|
6407
|
+
}
|
|
6408
|
+
async function consumeUpTo(handle, size) {
|
|
6409
|
+
const chunk = await readAt(handle, state.offset, size - state.offset);
|
|
6410
|
+
const completeLength = chunk.lastIndexOf(NEWLINE) + 1;
|
|
6411
|
+
if (completeLength > 0) {
|
|
6412
|
+
const parsed = parseJsonLines(chunk.subarray(0, completeLength).toString("utf8"), schema);
|
|
6413
|
+
const offset = state.offset + completeLength;
|
|
6414
|
+
state = { offset, headFingerprint: await headFingerprintAt(handle, offset), values: [...state.values, ...parsed.values], invalidLines: state.invalidLines + parsed.invalidLines };
|
|
6415
|
+
}
|
|
6416
|
+
const pending = chunk.subarray(completeLength);
|
|
6417
|
+
return { ...parseJsonLines(pending.toString("utf8"), schema), bytes: pending.length };
|
|
6418
|
+
}
|
|
6419
|
+
return { read };
|
|
6420
|
+
}
|
|
6421
|
+
function emptyState() {
|
|
6422
|
+
return { offset: 0, headFingerprint: EMPTY_FINGERPRINT, values: [], invalidLines: 0 };
|
|
6423
|
+
}
|
|
6424
|
+
function nothingPending() {
|
|
6425
|
+
return { values: [], invalidLines: 0, bytes: 0 };
|
|
6426
|
+
}
|
|
6427
|
+
async function headFingerprintAt(handle, offset) {
|
|
6428
|
+
const length = Math.min(offset, HEAD_FINGERPRINT_BYTES);
|
|
6429
|
+
if (length === 0) return EMPTY_FINGERPRINT;
|
|
6430
|
+
return createHash5("sha1").update(await readAt(handle, 0, length)).digest("hex");
|
|
6431
|
+
}
|
|
6432
|
+
|
|
6433
|
+
// src/server/stats-sources.ts
|
|
6434
|
+
var ALL_PROJECTS_SLOT = "*";
|
|
6435
|
+
function createStatsSources(root, computeCost) {
|
|
6436
|
+
const tails = /* @__PURE__ */ new Map();
|
|
6437
|
+
const bases = /* @__PURE__ */ new Map();
|
|
6438
|
+
const runsTail = createJsonlTail(join24(root, RUNS_FILE), cliRunSchema);
|
|
6439
|
+
const costMemos = /* @__PURE__ */ new Map();
|
|
6440
|
+
const snapshotIds = /* @__PURE__ */ new WeakMap();
|
|
6441
|
+
let lastSnapshotId = 0;
|
|
6442
|
+
const snapshotIdOf = (snapshot) => {
|
|
6443
|
+
const known = snapshotIds.get(snapshot);
|
|
6444
|
+
if (known !== void 0) return known;
|
|
6445
|
+
lastSnapshotId += 1;
|
|
6446
|
+
snapshotIds.set(snapshot, lastSnapshotId);
|
|
6447
|
+
return lastSnapshotId;
|
|
6448
|
+
};
|
|
6449
|
+
const tailOf = (projectId) => {
|
|
6450
|
+
const known = tails.get(projectId);
|
|
6451
|
+
if (known !== void 0) return known;
|
|
6452
|
+
const created = createJsonlTail(join24(root, projectId, JOURNAL_FILE), journalEventSchema);
|
|
6453
|
+
tails.set(projectId, created);
|
|
6454
|
+
return created;
|
|
6455
|
+
};
|
|
6456
|
+
const readTailed = async (projectId) => {
|
|
6457
|
+
const { length, generation, ...lines } = await tailOf(projectId).read();
|
|
6458
|
+
return { journal: projectJournal(projectId, lines), position: `${projectId}=${generation}:${length}` };
|
|
6459
|
+
};
|
|
6460
|
+
const rememberedBase = (snapshot, tailed) => (slot, input) => {
|
|
6461
|
+
const slotKey = `${slot}|${input.projectId ?? "*"}`;
|
|
6462
|
+
const positions = tailed.filter(({ journal }) => input.projectId === void 0 || journal.projectId === input.projectId).map(({ position }) => position);
|
|
6463
|
+
const key = `${snapshotIdOf(snapshot)}|${slotKey}|${positions.join(",")}`;
|
|
6464
|
+
const remembered2 = bases.get(slotKey);
|
|
6465
|
+
if (remembered2?.key === key) return remembered2.base;
|
|
6466
|
+
const base = reportBase(input);
|
|
6467
|
+
bases.set(slotKey, { projectId: input.projectId, key, base });
|
|
6468
|
+
return base;
|
|
6469
|
+
};
|
|
6470
|
+
return {
|
|
6471
|
+
read: async (snapshot, projectIds) => {
|
|
6472
|
+
const tailed = await Promise.all(projectIds.map(readTailed));
|
|
6473
|
+
return { journals: tailed.map(({ journal }) => journal), baseOf: rememberedBase(snapshot, tailed) };
|
|
6474
|
+
},
|
|
6475
|
+
retain: (projectIds) => {
|
|
6476
|
+
const kept = new Set(projectIds);
|
|
6477
|
+
pruneUnlessKept(tails, kept, (projectId) => projectId);
|
|
6478
|
+
pruneUnlessKept(bases, kept, (_, { projectId }) => projectId);
|
|
6479
|
+
pruneUnlessKept(costMemos, kept, (slot) => slot === ALL_PROJECTS_SLOT ? void 0 : slot);
|
|
6480
|
+
},
|
|
6481
|
+
costReport: async (request) => {
|
|
6482
|
+
const { values: runs, length, generation } = await runsTail.read();
|
|
6483
|
+
const inputs = { ...request, runs };
|
|
6484
|
+
const slot = inputs.scope.projectId ?? ALL_PROJECTS_SLOT;
|
|
6485
|
+
const keyParts = {
|
|
6486
|
+
usage: `${inputs.usage.revision}`,
|
|
6487
|
+
runs: `${generation}:${length}`,
|
|
6488
|
+
scope: `${snapshotIdOf(inputs.scope.snapshot)}/${slot}`,
|
|
6489
|
+
now: formatLocalDay(inputs.now)
|
|
6490
|
+
};
|
|
6491
|
+
const key = Object.values(keyParts).join("|");
|
|
6492
|
+
const remembered2 = costMemos.get(slot);
|
|
6493
|
+
if (remembered2?.key === key) return remembered2.report;
|
|
6494
|
+
const report = computeCost(inputs);
|
|
6495
|
+
costMemos.set(slot, { key, report });
|
|
6496
|
+
report.catch(() => {
|
|
6497
|
+
if (costMemos.get(slot)?.report === report) costMemos.delete(slot);
|
|
6498
|
+
});
|
|
6499
|
+
return report;
|
|
6500
|
+
}
|
|
6501
|
+
};
|
|
6502
|
+
}
|
|
6503
|
+
function pruneUnlessKept(map, kept, projectIdOf) {
|
|
6504
|
+
for (const [key, value] of map) {
|
|
6505
|
+
const projectId = projectIdOf(key, value);
|
|
6506
|
+
if (projectId !== void 0 && !kept.has(projectId)) map.delete(key);
|
|
6507
|
+
}
|
|
6508
|
+
}
|
|
6509
|
+
|
|
6237
6510
|
// src/server/stats-api.ts
|
|
6238
6511
|
var REPORT_TTL_MS = 5 * 60 * 1e3;
|
|
6239
|
-
|
|
6512
|
+
var ALL_PROJECTS_TAG = "project:*";
|
|
6513
|
+
var WHOLE_BACKLOG_TAG = "whole-backlog";
|
|
6514
|
+
var projectTag = (projectId) => `project:${projectId}`;
|
|
6515
|
+
function createStatsApi({ root, readLanguage: readLanguage2, now, home: home2, usage, memory, warn: warn2, backlog, graphHealth: graphHealth2 }) {
|
|
6240
6516
|
const routes = new Hono();
|
|
6241
6517
|
const reports = createReportCache({ ttlMs: REPORT_TTL_MS, now: () => now().getTime() });
|
|
6242
6518
|
const onCodeSourceError = (kind, error) => void readLanguage2().then((language) => {
|
|
@@ -6246,68 +6522,95 @@ function createStatsApi({ root, readLanguage: readLanguage2, now, home: home2, u
|
|
|
6246
6522
|
});
|
|
6247
6523
|
const codeSource = createCodeSource({ home: home2, store: createCodeCacheFile(root), onError: onCodeSourceError });
|
|
6248
6524
|
const lookupRepoRoot = cachedRepoRoots();
|
|
6525
|
+
const sources = createStatsSources(root, (inputs) => costOf2(inputs, home2, lookupRepoRoot));
|
|
6526
|
+
let knownProjectIds = [];
|
|
6527
|
+
let forgetCount = 0;
|
|
6249
6528
|
const statsScopeOf = async (c, { wholeBacklog }) => {
|
|
6250
6529
|
const projectId = c.req.query("project") || void 0;
|
|
6251
|
-
const
|
|
6530
|
+
const snapshot = await backlog();
|
|
6531
|
+
const { projects, tasks, errors } = snapshot;
|
|
6532
|
+
knownProjectIds = projects.map((project) => project.id);
|
|
6533
|
+
sources.retain(knownProjectIds);
|
|
6534
|
+
codeSource.retain(projects);
|
|
6252
6535
|
if (projectId !== void 0 && !projects.some((project) => project.id === projectId)) {
|
|
6253
6536
|
return c.json({ errors: [serverMessages(await readLanguage2()).projectNotFound(projectId)] }, 404);
|
|
6254
6537
|
}
|
|
6255
|
-
const
|
|
6256
|
-
const scoped = projects.filter(included);
|
|
6538
|
+
const scoped = projectsInScope(projects, projectId, { wholeBacklog });
|
|
6257
6539
|
const scopedIds = new Set(scoped.map((project) => project.id));
|
|
6258
6540
|
const inScope = (task) => scopedIds.has(task.projectId);
|
|
6259
|
-
return { projectId, projects: scoped, tasks: tasks.filter(inScope), unparsedTasks: unparsedTasks(errors).filter(inScope) };
|
|
6541
|
+
return { projectId, projects: scoped, tasks: tasks.filter(inScope), unparsedTasks: unparsedTasks(errors).filter(inScope), snapshot };
|
|
6260
6542
|
};
|
|
6261
6543
|
const scopedStats = (name, report, { sourceKey, wholeBacklog = false } = {}) => async (c) => {
|
|
6544
|
+
const forgetCountAtRead = forgetCount;
|
|
6262
6545
|
const scope = await statsScopeOf(c, { wholeBacklog });
|
|
6263
6546
|
if (scope instanceof Response) return scope;
|
|
6264
6547
|
const moment = now();
|
|
6265
6548
|
const key = [name, scope.projectId ?? "*", formatLocalDay(moment), sourceKey === void 0 ? "" : await sourceKey(scope.projects)].join("|");
|
|
6266
|
-
const
|
|
6267
|
-
|
|
6549
|
+
const tags = wholeBacklog ? [WHOLE_BACKLOG_TAG] : [scope.projectId === void 0 ? ALL_PROJECTS_TAG : projectTag(scope.projectId)];
|
|
6550
|
+
const compute = async () => {
|
|
6551
|
+
const { journals, baseOf } = await sources.read(scope.snapshot, scope.projects.map((project) => project.id));
|
|
6268
6552
|
const input = { tasks: scope.tasks, journals, now: moment, projectId: scope.projectId, unparsedTasks: scope.unparsedTasks };
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6553
|
+
const wholeBacklogBase = () => baseOf("backlog", { ...input, projectId: void 0 });
|
|
6554
|
+
return report({ input, base: baseOf("scoped", input), projects: scope.projects, snapshot: scope.snapshot, wholeBacklogBase });
|
|
6555
|
+
};
|
|
6556
|
+
const scopeOutdated = forgetCount !== forgetCountAtRead;
|
|
6557
|
+
return c.json(await (scopeOutdated ? compute() : reports.get(key, compute, tags)));
|
|
6272
6558
|
};
|
|
6273
|
-
const statsOfCode = async (input, base, projects) => codeReport({ ...input, code: await codeSource.collect(projects, input.now) }, base);
|
|
6274
|
-
const statsOfEffect = async (input, base, projects) => {
|
|
6275
|
-
const backlogBase = input.projectId === void 0 ? base :
|
|
6559
|
+
const statsOfCode = async ({ input, base, projects }) => codeReport({ ...input, code: await codeSource.collect(projects, input.now) }, base);
|
|
6560
|
+
const statsOfEffect = async ({ input, base, projects, wholeBacklogBase }) => {
|
|
6561
|
+
const backlogBase = input.projectId === void 0 ? base : wholeBacklogBase();
|
|
6276
6562
|
const scoped = projects.filter((project) => input.projectId === void 0 || project.id === input.projectId);
|
|
6277
6563
|
const code = await codeSource.collect(scoped, input.now);
|
|
6278
6564
|
const fixCommits = await codeSource.fixCommits(projects, codeFixRequests({ ...input, projectId: void 0 }, backlogBase), input.now);
|
|
6279
6565
|
return effectReport({ ...input, code: { ...code, fixCommits } }, base, backlogBase);
|
|
6280
6566
|
};
|
|
6281
|
-
const statsOfQuality = async (input, base, projects
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
const { cache, scan } = usage.snapshot();
|
|
6285
|
-
const buckets = bucketsOf2(cache);
|
|
6286
|
-
const runs = await readRuns(root);
|
|
6287
|
-
const repoRoots = projectId === void 0 ? /* @__PURE__ */ new Map() : await resolveRepoRoots(lookupRepoRoot, [...buckets, ...runs].map((entry) => entry.cwd));
|
|
6288
|
-
const projectOf2 = (cwd) => {
|
|
6289
|
-
const roots = repoRoots.get(cwd) ?? null;
|
|
6290
|
-
return roots === null ? null : findProjectForRoots(projects, roots, home2)?.id ?? null;
|
|
6291
|
-
};
|
|
6292
|
-
return costReport({ buckets, runs, projectOf: projectOf2, projectId, now: now(), scan });
|
|
6567
|
+
const statsOfQuality = async ({ input, base, projects, snapshot }) => {
|
|
6568
|
+
const graphs = await Promise.all(projects.map(async (project) => ({ projectId: project.id, name: project.name, ...await graphHealth2(snapshot, project) })));
|
|
6569
|
+
return qualityReport(input, base, graphs);
|
|
6293
6570
|
};
|
|
6294
6571
|
const codeState = { sourceKey: (projects) => codeSource.stateKey(projects) };
|
|
6295
|
-
routes.get("/stats", scopedStats("stats", (input, base) => statsReport(input, base)));
|
|
6572
|
+
routes.get("/stats", scopedStats("stats", ({ input, base }) => statsReport(input, base)));
|
|
6296
6573
|
routes.get("/stats/code", scopedStats("code", statsOfCode, codeState));
|
|
6297
6574
|
routes.get("/stats/effect", scopedStats("effect", statsOfEffect, { ...codeState, wholeBacklog: true }));
|
|
6298
6575
|
routes.get("/stats/quality", scopedStats("quality", statsOfQuality));
|
|
6299
|
-
routes.get("/stats/signals", scopedStats("signals", (input, base) => ({ signals: statsSignals(input, base) })));
|
|
6576
|
+
routes.get("/stats/signals", scopedStats("signals", ({ input, base }) => ({ signals: statsSignals(input, base) })));
|
|
6300
6577
|
routes.get("/stats/cost", async (c) => {
|
|
6301
6578
|
const scope = await statsScopeOf(c, { wholeBacklog: true });
|
|
6302
|
-
|
|
6579
|
+
if (scope instanceof Response) return scope;
|
|
6580
|
+
usage.ensureStarted();
|
|
6581
|
+
const { snapshot, projectId } = scope;
|
|
6582
|
+
return c.json(await sources.costReport({ usage: usage.snapshot(), scope: { snapshot, projectId }, now: now() }));
|
|
6303
6583
|
});
|
|
6304
6584
|
routes.get("/stats/memory", (c) => c.json({ samples: memory.samples() }));
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6585
|
+
const projectIdOfPath = (path) => {
|
|
6586
|
+
const [first] = relative3(root, path).split(sep4);
|
|
6587
|
+
return knownProjectIds.find((id) => id === first);
|
|
6588
|
+
};
|
|
6589
|
+
const forget = (paths) => {
|
|
6590
|
+
forgetCount += 1;
|
|
6591
|
+
if (paths === void 0 || paths.length === 0) return void reports.clear();
|
|
6592
|
+
const changedProjectIds = /* @__PURE__ */ new Set();
|
|
6593
|
+
for (const path of paths) {
|
|
6594
|
+
const projectId = projectIdOfPath(path);
|
|
6595
|
+
if (projectId === void 0) return void reports.clear();
|
|
6596
|
+
changedProjectIds.add(projectId);
|
|
6597
|
+
}
|
|
6598
|
+
reports.clearTagged([ALL_PROJECTS_TAG, WHOLE_BACKLOG_TAG, ...[...changedProjectIds].map(projectTag)]);
|
|
6599
|
+
};
|
|
6600
|
+
return { routes, forget };
|
|
6601
|
+
}
|
|
6602
|
+
function projectsInScope(projects, projectId, { wholeBacklog }) {
|
|
6603
|
+
return projects.filter((project) => project.id === projectId || (projectId === void 0 || wholeBacklog) && project.active);
|
|
6604
|
+
}
|
|
6605
|
+
async function costOf2({ usage: { cache, scan }, runs, scope: { snapshot, projectId }, now }, home2, lookupRepoRoot) {
|
|
6606
|
+
const projects = projectsInScope(snapshot.projects, projectId, { wholeBacklog: true });
|
|
6607
|
+
const buckets = bucketsOf2(cache);
|
|
6608
|
+
const repoRoots = projectId === void 0 ? /* @__PURE__ */ new Map() : await resolveRepoRoots(lookupRepoRoot, [...buckets, ...runs].map((entry) => entry.cwd));
|
|
6609
|
+
const projectOf2 = (cwd) => {
|
|
6610
|
+
const roots = repoRoots.get(cwd) ?? null;
|
|
6611
|
+
return roots === null ? null : findProjectForRoots(projects, roots, home2)?.id ?? null;
|
|
6612
|
+
};
|
|
6613
|
+
return costReport({ buckets, runs, projectOf: projectOf2, projectId, now, scan });
|
|
6311
6614
|
}
|
|
6312
6615
|
function bucketsOf2(cache) {
|
|
6313
6616
|
return Object.values(cache.files).flatMap((entry) => entry.buckets);
|
|
@@ -6330,30 +6633,35 @@ function createApi({ root, readLanguage: readLanguage2, changes, now, home: home
|
|
|
6330
6633
|
});
|
|
6331
6634
|
return snapshot;
|
|
6332
6635
|
};
|
|
6333
|
-
const
|
|
6334
|
-
const
|
|
6335
|
-
|
|
6636
|
+
const graphHealthsBySnapshot = /* @__PURE__ */ new WeakMap();
|
|
6637
|
+
const graphHealth2 = (backlogSnapshot, project) => {
|
|
6638
|
+
let healths = graphHealthsBySnapshot.get(backlogSnapshot);
|
|
6639
|
+
if (healths === void 0) {
|
|
6640
|
+
healths = createReportCache({ ttlMs: GRAPH_STATE_TTL_MS, now: () => now().getTime() });
|
|
6641
|
+
graphHealthsBySnapshot.set(backlogSnapshot, healths);
|
|
6642
|
+
}
|
|
6643
|
+
return healths.get(project.id, () => projectGraphHealth(project, backlogSnapshot.tasks, home2));
|
|
6644
|
+
};
|
|
6645
|
+
const stats = createStatsApi({ root, readLanguage: readLanguage2, now, home: home2, usage, memory, warn: warn2, backlog, graphHealth: graphHealth2 });
|
|
6646
|
+
const forgetBacklog = (paths) => {
|
|
6336
6647
|
snapshot = null;
|
|
6337
|
-
stats.forget();
|
|
6338
|
-
graphStates.clear();
|
|
6648
|
+
stats.forget(paths);
|
|
6339
6649
|
};
|
|
6340
6650
|
const recordOwnWrites = async (writes) => {
|
|
6341
6651
|
if (writes.length > 0) await revisions.recordOwnWrites(writes);
|
|
6342
|
-
forgetBacklog();
|
|
6652
|
+
forgetBacklog(writes.length > 0 ? writes.map((write) => write.path) : void 0);
|
|
6343
6653
|
};
|
|
6344
6654
|
const streams = /* @__PURE__ */ new Set();
|
|
6345
6655
|
changes.subscribe(async (paths) => {
|
|
6346
|
-
if (await revisions.settle(paths) === "foreign") forgetBacklog();
|
|
6347
|
-
else stats.forget();
|
|
6656
|
+
if (await revisions.settle(paths) === "foreign") forgetBacklog(paths);
|
|
6657
|
+
else stats.forget(paths);
|
|
6348
6658
|
const revision = revisions.current();
|
|
6349
6659
|
for (const send of streams) send(revision);
|
|
6350
6660
|
});
|
|
6351
6661
|
api.get("/projects", async (c) => {
|
|
6352
|
-
const
|
|
6353
|
-
const
|
|
6354
|
-
|
|
6355
|
-
return { ...project, codeGraph: codeGraph2 };
|
|
6356
|
-
};
|
|
6662
|
+
const loaded = await backlog();
|
|
6663
|
+
const { projects, revision } = loaded;
|
|
6664
|
+
const withGraph = async (project) => ({ ...project, codeGraph: (await graphHealth2(loaded, project)).state });
|
|
6357
6665
|
return c.json({ projects: await Promise.all(projects.map(withGraph)), revision });
|
|
6358
6666
|
});
|
|
6359
6667
|
api.get("/tasks", async (c) => {
|
|
@@ -6507,13 +6815,13 @@ function createApp({ root, readLanguage: readLanguage2, changes, allowedHosts, h
|
|
|
6507
6815
|
});
|
|
6508
6816
|
if (staticDir !== void 0) {
|
|
6509
6817
|
app.use("*", serveStatic({ root: staticDir }));
|
|
6510
|
-
app.get("*", serveStatic({ path:
|
|
6818
|
+
app.get("*", serveStatic({ path: join25(staticDir, "index.html") }));
|
|
6511
6819
|
}
|
|
6512
6820
|
return app;
|
|
6513
6821
|
}
|
|
6514
6822
|
|
|
6515
6823
|
// src/server/change-feed.ts
|
|
6516
|
-
import { relative as
|
|
6824
|
+
import { relative as relative4, sep as sep5 } from "node:path";
|
|
6517
6825
|
import { watch } from "chokidar";
|
|
6518
6826
|
var CHANGE_DEBOUNCE_MS = 100;
|
|
6519
6827
|
function createDebouncer(delayMs, run) {
|
|
@@ -6527,7 +6835,7 @@ function createDebouncer(delayMs, run) {
|
|
|
6527
6835
|
};
|
|
6528
6836
|
}
|
|
6529
6837
|
function isHiddenPath(root, path) {
|
|
6530
|
-
return
|
|
6838
|
+
return relative4(root, path).split(sep5).some((segment) => segment.startsWith("."));
|
|
6531
6839
|
}
|
|
6532
6840
|
function createChangeFeed({ root, debounceMs, messages, warn: warn2 }) {
|
|
6533
6841
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -6618,10 +6926,13 @@ function logReport({ closedEpics, reopenedEpics, blockingFiles, deleted, conflic
|
|
|
6618
6926
|
}
|
|
6619
6927
|
}
|
|
6620
6928
|
|
|
6929
|
+
// src/server/usage-scanner.ts
|
|
6930
|
+
import { isDeepStrictEqual } from "node:util";
|
|
6931
|
+
|
|
6621
6932
|
// src/core/usage/transcripts.ts
|
|
6622
|
-
import { createHash as
|
|
6623
|
-
import {
|
|
6624
|
-
import { basename as basename10, join as
|
|
6933
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
6934
|
+
import { stat as stat6 } from "node:fs/promises";
|
|
6935
|
+
import { basename as basename10, join as join27 } from "node:path";
|
|
6625
6936
|
|
|
6626
6937
|
// src/core/stats/cost/attribute.ts
|
|
6627
6938
|
import { z as z18 } from "zod";
|
|
@@ -6955,7 +7266,7 @@ function slotOf(timestamp) {
|
|
|
6955
7266
|
|
|
6956
7267
|
// src/core/usage/usage-cache.ts
|
|
6957
7268
|
import { mkdir as mkdir5 } from "node:fs/promises";
|
|
6958
|
-
import { join as
|
|
7269
|
+
import { join as join26 } from "node:path";
|
|
6959
7270
|
import { z as z19 } from "zod";
|
|
6960
7271
|
var USAGE_CACHE_FILE = ".usage-cache.json";
|
|
6961
7272
|
var USAGE_CACHE_VERSION = 6;
|
|
@@ -6975,33 +7286,32 @@ function emptyUsageCache() {
|
|
|
6975
7286
|
return { version: USAGE_CACHE_VERSION, files: {} };
|
|
6976
7287
|
}
|
|
6977
7288
|
async function readUsageCache(root) {
|
|
6978
|
-
return await readJsonFile(
|
|
7289
|
+
return await readJsonFile(join26(root, USAGE_CACHE_FILE), usageCacheSchema) ?? emptyUsageCache();
|
|
6979
7290
|
}
|
|
6980
7291
|
async function writeUsageCache(root, cache) {
|
|
6981
7292
|
await mkdir5(root, { recursive: true });
|
|
6982
|
-
await writeFileAtomic(
|
|
7293
|
+
await writeFileAtomic(join26(root, USAGE_CACHE_FILE), JSON.stringify(cache));
|
|
6983
7294
|
}
|
|
6984
7295
|
|
|
6985
7296
|
// src/core/usage/transcripts.ts
|
|
6986
|
-
var NEWLINE = 10;
|
|
6987
7297
|
var FINGERPRINT_BYTES = 256;
|
|
6988
7298
|
var ABANDONED_LINE_MS = 10 * 60 * 1e3;
|
|
6989
|
-
var
|
|
7299
|
+
var EMPTY_FINGERPRINT2 = createHash6("sha1").digest("hex");
|
|
6990
7300
|
var COUNTED_LINE_MARKERS = ['"type":"assistant"', '"type":"user"'];
|
|
6991
7301
|
async function listTranscripts(claudeProjectsDir2) {
|
|
6992
7302
|
const files = [];
|
|
6993
7303
|
for (const projectEntry of await listDir(claudeProjectsDir2)) {
|
|
6994
7304
|
if (!projectEntry.isDirectory()) continue;
|
|
6995
|
-
const projectDir2 =
|
|
7305
|
+
const projectDir2 = join27(claudeProjectsDir2, projectEntry.name);
|
|
6996
7306
|
for (const entry of await listDir(projectDir2)) {
|
|
6997
|
-
const entryPath =
|
|
7307
|
+
const entryPath = join27(projectDir2, entry.name);
|
|
6998
7308
|
if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
6999
7309
|
files.push(...await listedFile(entryPath));
|
|
7000
7310
|
continue;
|
|
7001
7311
|
}
|
|
7002
7312
|
if (!entry.isDirectory()) continue;
|
|
7003
|
-
for (const subagentEntry of await listDir(
|
|
7004
|
-
if (subagentEntry.isFile() && subagentEntry.name.endsWith(".jsonl")) files.push(...await listedFile(
|
|
7313
|
+
for (const subagentEntry of await listDir(join27(entryPath, "subagents"), { recursive: true })) {
|
|
7314
|
+
if (subagentEntry.isFile() && subagentEntry.name.endsWith(".jsonl")) files.push(...await listedFile(join27(subagentEntry.parentPath, subagentEntry.name)));
|
|
7005
7315
|
}
|
|
7006
7316
|
}
|
|
7007
7317
|
}
|
|
@@ -7045,7 +7355,7 @@ function deletedStillReported(cache, listedFiles, now) {
|
|
|
7045
7355
|
}
|
|
7046
7356
|
async function scanChunk(file, start, chunkSize, { longestReadableLine, now }) {
|
|
7047
7357
|
if (chunkSize <= 0) return { size: file.size, offset: start.offset, state: start.state, buckets: start.buckets };
|
|
7048
|
-
const chunk = await
|
|
7358
|
+
const chunk = await readFileAt(file.path, start.offset, chunkSize);
|
|
7049
7359
|
const tailAbandoned = start.offset + chunk.length === file.size && now.getTime() - file.mtimeMs >= ABANDONED_LINE_MS;
|
|
7050
7360
|
const readableLength = tailAbandoned ? chunk.length : chunk.lastIndexOf(NEWLINE) + 1;
|
|
7051
7361
|
if (readableLength === 0) {
|
|
@@ -7069,30 +7379,14 @@ async function continues(file, previous) {
|
|
|
7069
7379
|
return await fingerprintOf(file.path, previous.offset) === previous.fingerprint;
|
|
7070
7380
|
}
|
|
7071
7381
|
async function fingerprintOf(path, offset) {
|
|
7072
|
-
if (offset === 0) return
|
|
7382
|
+
if (offset === 0) return EMPTY_FINGERPRINT2;
|
|
7073
7383
|
const edge = Math.min(FINGERPRINT_BYTES, offset);
|
|
7074
7384
|
return withFile(path, async (handle) => {
|
|
7075
7385
|
const head = await readAt(handle, 0, edge);
|
|
7076
7386
|
const tail = await readAt(handle, offset - edge, edge);
|
|
7077
|
-
return
|
|
7387
|
+
return createHash6("sha1").update(head).update(tail).digest("hex");
|
|
7078
7388
|
});
|
|
7079
7389
|
}
|
|
7080
|
-
function readChunk(path, position, length) {
|
|
7081
|
-
return withFile(path, (handle) => readAt(handle, position, length));
|
|
7082
|
-
}
|
|
7083
|
-
async function withFile(path, use) {
|
|
7084
|
-
const handle = await open2(path, "r");
|
|
7085
|
-
try {
|
|
7086
|
-
return await use(handle);
|
|
7087
|
-
} finally {
|
|
7088
|
-
await handle.close();
|
|
7089
|
-
}
|
|
7090
|
-
}
|
|
7091
|
-
async function readAt(handle, position, length) {
|
|
7092
|
-
const buffer = Buffer.alloc(length);
|
|
7093
|
-
const { bytesRead } = await handle.read(buffer, 0, length, position);
|
|
7094
|
-
return buffer.subarray(0, bytesRead);
|
|
7095
|
-
}
|
|
7096
7390
|
function parseLineOrNull(line) {
|
|
7097
7391
|
try {
|
|
7098
7392
|
return JSON.parse(line);
|
|
@@ -7131,17 +7425,24 @@ function createUsageScanner({
|
|
|
7131
7425
|
}) {
|
|
7132
7426
|
let cache = null;
|
|
7133
7427
|
let scan = NOT_LISTED;
|
|
7428
|
+
let revision = 0;
|
|
7134
7429
|
let inFlight = null;
|
|
7135
7430
|
let timer = null;
|
|
7136
7431
|
let running = false;
|
|
7137
7432
|
let budgetExhausted = false;
|
|
7433
|
+
const setScan = (next) => {
|
|
7434
|
+
if (!scanEquals(scan, next)) revision += 1;
|
|
7435
|
+
scan = next;
|
|
7436
|
+
};
|
|
7138
7437
|
const runPass = async () => {
|
|
7438
|
+
const published = cache ?? emptyUsageCache();
|
|
7139
7439
|
const current = cache ?? await readUsageCache(root);
|
|
7140
7440
|
const files = await listTranscripts(claudeProjectsDir2);
|
|
7141
|
-
|
|
7441
|
+
setScan(progressBefore(files, current));
|
|
7142
7442
|
const result = await scanTranscripts({ files, cache: current, byteBudget, now: now() });
|
|
7443
|
+
if (cacheChanged(published, result.cache)) revision += 1;
|
|
7143
7444
|
cache = result.cache;
|
|
7144
|
-
|
|
7445
|
+
setScan({ listed: true, filesTotal: files.length, filesDone: result.filesDone, bytesLeft: result.bytesLeft });
|
|
7145
7446
|
budgetExhausted = result.bytesRead >= byteBudget;
|
|
7146
7447
|
if (result.bytesRead > 0) await writeUsageCache(root, result.cache);
|
|
7147
7448
|
};
|
|
@@ -7173,9 +7474,21 @@ function createUsageScanner({
|
|
|
7173
7474
|
ensureStarted: () => {
|
|
7174
7475
|
if (!scan.listed && inFlight === null) void scanOnce();
|
|
7175
7476
|
},
|
|
7176
|
-
snapshot: () => ({ cache: cache ?? emptyUsageCache(), scan })
|
|
7477
|
+
snapshot: () => ({ cache: cache ?? emptyUsageCache(), scan, revision })
|
|
7177
7478
|
};
|
|
7178
7479
|
}
|
|
7480
|
+
function scanEquals(a, b) {
|
|
7481
|
+
return a.listed === b.listed && a.filesTotal === b.filesTotal && a.filesDone === b.filesDone && a.bytesLeft === b.bytesLeft;
|
|
7482
|
+
}
|
|
7483
|
+
function cacheChanged(previous, next) {
|
|
7484
|
+
const previousPaths = Object.keys(previous.files);
|
|
7485
|
+
if (previousPaths.length !== Object.keys(next.files).length) return true;
|
|
7486
|
+
return previousPaths.some((path) => entryChanged(previous.files[path], next.files[path]));
|
|
7487
|
+
}
|
|
7488
|
+
function entryChanged(previous, next) {
|
|
7489
|
+
if (previous === void 0 || next === void 0) return previous !== next;
|
|
7490
|
+
return previous.size !== next.size || previous.offset !== next.offset || previous.fingerprint !== next.fingerprint || !isDeepStrictEqual(previous.buckets, next.buckets);
|
|
7491
|
+
}
|
|
7179
7492
|
function progressBefore(files, cache) {
|
|
7180
7493
|
const offsetOf = (file) => {
|
|
7181
7494
|
const cached = cache.files[file.path];
|
|
@@ -7192,7 +7505,7 @@ function progressBefore(files, cache) {
|
|
|
7192
7505
|
// src/server/start.ts
|
|
7193
7506
|
var SWEEP_INTERVAL_MS = 60 * 60 * 1e3;
|
|
7194
7507
|
var STOP_SIGNALS = ["SIGTERM", "SIGINT", "SIGHUP"];
|
|
7195
|
-
var BUNDLED_WEB_DIR =
|
|
7508
|
+
var BUNDLED_WEB_DIR = join28(import.meta.dirname, "web");
|
|
7196
7509
|
var log = (line) => void process.stdout.write(`${line}
|
|
7197
7510
|
`);
|
|
7198
7511
|
var warn = (line) => void process.stderr.write(`${line}
|
|
@@ -7293,7 +7606,7 @@ async function runServe(args, io) {
|
|
|
7293
7606
|
|
|
7294
7607
|
// src/cli/service/launchd.ts
|
|
7295
7608
|
import { mkdir as mkdir7, rm as rm5, writeFile as writeFile4 } from "node:fs/promises";
|
|
7296
|
-
import { dirname as dirname8, join as
|
|
7609
|
+
import { dirname as dirname8, join as join29, posix } from "node:path";
|
|
7297
7610
|
import { setTimeout as sleep3 } from "node:timers/promises";
|
|
7298
7611
|
|
|
7299
7612
|
// src/cli/service/service.ts
|
|
@@ -7366,7 +7679,7 @@ ${entries}
|
|
|
7366
7679
|
`;
|
|
7367
7680
|
}
|
|
7368
7681
|
function launchdManager(context, delay = sleep3) {
|
|
7369
|
-
const file =
|
|
7682
|
+
const file = join29(context.home, "Library/LaunchAgents", `${LABEL}.plist`);
|
|
7370
7683
|
const domain = `gui/${context.uid}`;
|
|
7371
7684
|
const bootout = () => context.exec("launchctl", ["bootout", `${domain}/${LABEL}`]);
|
|
7372
7685
|
const bootstrap = () => context.exec("launchctl", ["bootstrap", domain, file]);
|
|
@@ -7401,7 +7714,7 @@ function launchdManager(context, delay = sleep3) {
|
|
|
7401
7714
|
|
|
7402
7715
|
// src/cli/service/startup-folder.ts
|
|
7403
7716
|
import { mkdir as mkdir8, rm as rm6, writeFile as writeFile5 } from "node:fs/promises";
|
|
7404
|
-
import { dirname as dirname9, join as
|
|
7717
|
+
import { dirname as dirname9, join as join30, win32 } from "node:path";
|
|
7405
7718
|
var SCRIPT_NAME = "p-backlog.vbs";
|
|
7406
7719
|
var COMMAND_LINE_ARGUMENT = /"[^"]*"|\S+/g;
|
|
7407
7720
|
var PROCESS_QUERY_TIMEOUT_MS = 15e3;
|
|
@@ -7409,16 +7722,16 @@ function vbsString(value) {
|
|
|
7409
7722
|
return `"${value.replaceAll('"', '""')}"`;
|
|
7410
7723
|
}
|
|
7411
7724
|
function appDataDir(context) {
|
|
7412
|
-
return
|
|
7725
|
+
return join30(context.env.LOCALAPPDATA ?? join30(context.home, "AppData", "Local"), "p-backlog");
|
|
7413
7726
|
}
|
|
7414
7727
|
function startupFolder(context) {
|
|
7415
|
-
return
|
|
7728
|
+
return join30(context.env.APPDATA ?? join30(context.home, "AppData", "Roaming"), "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
|
|
7416
7729
|
}
|
|
7417
7730
|
function logPath2(context) {
|
|
7418
|
-
return
|
|
7731
|
+
return join30(appDataDir(context), "p-backlog.log");
|
|
7419
7732
|
}
|
|
7420
7733
|
function pidFilePath(context) {
|
|
7421
|
-
return
|
|
7734
|
+
return join30(appDataDir(context), "server.pid");
|
|
7422
7735
|
}
|
|
7423
7736
|
var NODE_PATH_ENV = "P_BACKLOG_NODE";
|
|
7424
7737
|
var CLI_PATH_ENV = "P_BACKLOG_CLI";
|
|
@@ -7469,7 +7782,7 @@ function sameWindowsPath(left, right) {
|
|
|
7469
7782
|
return win32.normalize(left).toLowerCase() === win32.normalize(right).toLowerCase();
|
|
7470
7783
|
}
|
|
7471
7784
|
function startupFolderManager(context) {
|
|
7472
|
-
const file =
|
|
7785
|
+
const file = join30(startupFolder(context), SCRIPT_NAME);
|
|
7473
7786
|
return {
|
|
7474
7787
|
file,
|
|
7475
7788
|
logs: logPath2(context),
|
|
@@ -7496,7 +7809,7 @@ function startupFolderManager(context) {
|
|
|
7496
7809
|
|
|
7497
7810
|
// src/cli/service/systemd.ts
|
|
7498
7811
|
import { mkdir as mkdir9, rm as rm7, writeFile as writeFile6 } from "node:fs/promises";
|
|
7499
|
-
import { dirname as dirname10, join as
|
|
7812
|
+
import { dirname as dirname10, join as join31 } from "node:path";
|
|
7500
7813
|
var UNIT = "p-backlog.service";
|
|
7501
7814
|
function quoted(value) {
|
|
7502
7815
|
return `"${value.replace(/[\\"]/g, "\\$&").replaceAll("%", "%%")}"`;
|
|
@@ -7530,7 +7843,7 @@ async function systemctlSteps(exec, steps) {
|
|
|
7530
7843
|
return "done";
|
|
7531
7844
|
}
|
|
7532
7845
|
function systemdManager(context) {
|
|
7533
|
-
const file =
|
|
7846
|
+
const file = join31(context.home, ".config/systemd/user", UNIT);
|
|
7534
7847
|
return {
|
|
7535
7848
|
file,
|
|
7536
7849
|
logs: "journalctl --user -u p-backlog",
|
|
@@ -7648,10 +7961,10 @@ function reportFailure({ failed, code, output }, io) {
|
|
|
7648
7961
|
}
|
|
7649
7962
|
|
|
7650
7963
|
// src/cli/commands/setup.ts
|
|
7651
|
-
import { join as
|
|
7964
|
+
import { join as join33 } from "node:path";
|
|
7652
7965
|
|
|
7653
7966
|
// src/cli/agents/agent-hooks.ts
|
|
7654
|
-
import { join as
|
|
7967
|
+
import { join as join32 } from "node:path";
|
|
7655
7968
|
|
|
7656
7969
|
// src/cli/agents/grouped-stop-hooks.ts
|
|
7657
7970
|
import { z as z20 } from "zod";
|
|
@@ -7799,7 +8112,7 @@ async function removeCursorStopHook(path, isOurs) {
|
|
|
7799
8112
|
var CODEX_HOOK_TIMEOUT_SECONDS = 30;
|
|
7800
8113
|
var AGENT_HOOKS_FILE = "hooks.json";
|
|
7801
8114
|
function agentHookConfigPath(agent, places) {
|
|
7802
|
-
return agent === "claude" ? claudeSettingsPath(places.env, places.home) :
|
|
8115
|
+
return agent === "claude" ? claudeSettingsPath(places.env, places.home) : join32(agentHomeDir(agent, places), AGENT_HOOKS_FILE);
|
|
7803
8116
|
}
|
|
7804
8117
|
function installAgentHook(agent, site) {
|
|
7805
8118
|
const path = agentHookConfigPath(agent, site);
|
|
@@ -7895,7 +8208,7 @@ async function setUpAgent(agent, io) {
|
|
|
7895
8208
|
async function linkAgentSkill(agent, io, voice) {
|
|
7896
8209
|
const messages = cliMessages(io.language);
|
|
7897
8210
|
const skillsDir = agentSkillsDir(agent, io);
|
|
7898
|
-
const target =
|
|
8211
|
+
const target = join33(skillsDir, "backlog");
|
|
7899
8212
|
const source = skillSourceDir(io.packageRoot, io.language);
|
|
7900
8213
|
let link2;
|
|
7901
8214
|
try {
|
|
@@ -7914,7 +8227,7 @@ async function linkAgentSkill(agent, io, voice) {
|
|
|
7914
8227
|
}
|
|
7915
8228
|
async function removeLegacySkillLinks(agent, io, voice) {
|
|
7916
8229
|
for (const legacyDir of legacySkillsDirs(agent, io)) {
|
|
7917
|
-
if (await unlinkOurSkill(legacyDir) === "removed") voice.print(cliMessages(io.language).manualSkillRemoval.removed(
|
|
8230
|
+
if (await unlinkOurSkill(legacyDir) === "removed") voice.print(cliMessages(io.language).manualSkillRemoval.removed(join33(legacyDir, "backlog")));
|
|
7918
8231
|
}
|
|
7919
8232
|
}
|
|
7920
8233
|
function reportHook(result, configPath, io, voice) {
|
|
@@ -7935,7 +8248,7 @@ async function removeManualSetup(agent, removing, io) {
|
|
|
7935
8248
|
const voice = agentVoice(agent, io);
|
|
7936
8249
|
const messages = cliMessages(io.language);
|
|
7937
8250
|
const skillsDir = agentSkillsDir(agent, io);
|
|
7938
|
-
const target =
|
|
8251
|
+
const target = join33(skillsDir, "backlog");
|
|
7939
8252
|
const sharer = await remainingSkillDirUser(agent, removing, io);
|
|
7940
8253
|
voice.print(sharer === null ? messages.manualSkillRemoval[await unlinkOurSkill(skillsDir)](target) : messages.manualSkillShared(target, AGENT_LABELS[sharer]));
|
|
7941
8254
|
await removeLegacySkillLinks(agent, io, voice);
|
|
@@ -7990,7 +8303,7 @@ function backlogAgeWeeks(histories, now) {
|
|
|
7990
8303
|
return firstCreated === null ? null : (now.getTime() - firstCreated) / WEEK_MS;
|
|
7991
8304
|
}
|
|
7992
8305
|
var clampedWeeks = (weeks) => Math.min(FORECAST_WINDOW_WEEKS, Math.max(MIN_WINDOW_WEEKS, weeks));
|
|
7993
|
-
function flowForecast(histories,
|
|
8306
|
+
function flowForecast(histories, open2, now) {
|
|
7994
8307
|
const ageWeeks = backlogAgeWeeks(histories, now);
|
|
7995
8308
|
const windowWeeks = ageWeeks === null ? FORECAST_WINDOW_WEEKS : clampedWeeks(Math.floor(ageWeeks) + 1);
|
|
7996
8309
|
const observedWeeks = ageWeeks === null ? FORECAST_WINDOW_WEEKS : clampedWeeks(ageWeeks);
|
|
@@ -7999,9 +8312,9 @@ function flowForecast(histories, open3, now) {
|
|
|
7999
8312
|
const closed = histories.flatMap(closingsOf).filter((closing) => inWindow(closing.at)).length;
|
|
8000
8313
|
const created = histories.filter((history) => inWindow(history.createdAt)).length;
|
|
8001
8314
|
const weeklyNet = (closed - created) / observedWeeks;
|
|
8002
|
-
const weeks =
|
|
8315
|
+
const weeks = open2 > 0 && weeklyNet > 0 ? Math.ceil(open2 / weeklyNet) : null;
|
|
8003
8316
|
const windowDays = Math.round(observedWeeks * DAYS_PER_WEEK);
|
|
8004
|
-
return { closed, created, open:
|
|
8317
|
+
return { closed, created, open: open2, weeklyNet, weeks, until: weeks === null ? null : formatLocalIso(weeksLater(now, weeks)), windowDays };
|
|
8005
8318
|
}
|
|
8006
8319
|
function weeksLater(now, weeks) {
|
|
8007
8320
|
return new Date(now.getFullYear(), now.getMonth(), now.getDate() + DAYS_PER_WEEK * weeks, now.getHours(), now.getMinutes(), now.getSeconds());
|
|
@@ -8086,7 +8399,7 @@ var statusCommand = taskFieldCommand({
|
|
|
8086
8399
|
|
|
8087
8400
|
// src/cli/commands/take.ts
|
|
8088
8401
|
import { realpathSync as realpathSync3 } from "node:fs";
|
|
8089
|
-
import { relative as
|
|
8402
|
+
import { relative as relative5, resolve as resolve5, sep as sep6 } from "node:path";
|
|
8090
8403
|
var takeCommand = {
|
|
8091
8404
|
name: "take",
|
|
8092
8405
|
usage: (language) => cliMessages(language).takeUsage(),
|
|
@@ -8161,7 +8474,7 @@ function isInside(file, path) {
|
|
|
8161
8474
|
function repoRelativePath(io, project, path) {
|
|
8162
8475
|
const roots = findGitRoots(io.cwd);
|
|
8163
8476
|
if (roots === null || findProjectForDir([project], io.cwd, io.home) === void 0) return sourcePath(path);
|
|
8164
|
-
const fromRoot =
|
|
8477
|
+
const fromRoot = relative5(roots.worktree, resolve5(realpathSync3(io.cwd), sourcePath(path))).split(sep6).join("/");
|
|
8165
8478
|
const outsideRepo = fromRoot === ".." || fromRoot.startsWith("../");
|
|
8166
8479
|
return outsideRepo ? sourcePath(path) : fromRoot;
|
|
8167
8480
|
}
|