co-maintainer 0.4.11 → 0.4.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "co-maintainer",
3
- "version": "0.4.11",
3
+ "version": "0.4.13",
4
4
  "description": "Analyzes a GitHub repository and writes repository-specific contribution guidance.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  import type { GitHubClient, Options } from "../types.ts";
2
2
  import type { PullRequest, Source, State } from "../knowledge/types.ts";
3
- export declare function collectSource(client: GitHubClient, options: Options, previous?: State, progress?: (items: PullRequest[]) => Promise<void>, codebaseProgress?: (data: {
3
+ export declare function collectSource(client: GitHubClient, options: Options, previous?: State, progress?: (items: PullRequest[], cache: PullRequest[]) => Promise<void>, codebaseProgress?: (data: {
4
4
  tree: string[];
5
5
  treeSha: Record<string, string>;
6
6
  files: Record<string, string>;
@@ -248,8 +248,10 @@ async function pullRequests(client, options, previous, phase, progress) {
248
248
  if (phase)
249
249
  phase.text = "listing pull requests · page 0 · fetched 0";
250
250
  const selected = await listPullRequestPages(client, options, phase);
251
- const previousByNumber = new Map((previous?.source.pullRequests ?? []).map((pr) => [pr.number, pr]));
251
+ const keptByNumber = new Map((previous?.source.pullRequests ?? []).map((pr) => [pr.number, pr]));
252
+ const cacheByNumber = new Map((previous?.source.pullRequestCache ?? []).map((pr) => [pr.number, pr]));
252
253
  const result = new Array(selected.length);
254
+ const cache = new Array(selected.length);
253
255
  let completed = 0;
254
256
  let save = Promise.resolve();
255
257
  if (phase) {
@@ -258,7 +260,8 @@ async function pullRequests(client, options, previous, phase, progress) {
258
260
  log("fetch", `${selected.length} pull requests · concurrency=${options.ghConcurrent}`);
259
261
  await mapPool(selected, options.ghConcurrent, async (pr, index) => {
260
262
  const number = Number(pr.number);
261
- const cached = previousByNumber.get(number);
263
+ const full = keptByNumber.get(number);
264
+ const cached = full ?? cacheByNumber.get(number);
262
265
  const listedAdditions = Number(pr.additions);
263
266
  const listedDeletions = Number(pr.deletions);
264
267
  const listedStatsAvailable = Number.isFinite(listedAdditions) &&
@@ -308,10 +311,12 @@ async function pullRequests(client, options, previous, phase, progress) {
308
311
  current.changesRequested = cached.changesRequested;
309
312
  }
310
313
  const only = options.onlyRequestChangedPr;
311
- const discussionUnchanged = cached?.updatedAt === current.updatedAt;
314
+ const discussionUnchanged = full?.updatedAt === current.updatedAt;
312
315
  const decisionKnown = typeof cached?.changesRequested === "boolean";
313
- const diffUnchanged = cached?.headSha === current.headSha && Boolean(cached?.diff);
314
- let droppedFromCache = only && discussionUnchanged && cached?.changesRequested === false;
316
+ const diffUnchanged = full?.headSha === current.headSha && Boolean(full?.diff);
317
+ let droppedFromCache = only &&
318
+ cached?.updatedAt === current.updatedAt &&
319
+ cached.changesRequested === false;
315
320
  let dropped = droppedFromCache;
316
321
  const loadComments = async () => {
317
322
  const comments = await client.pages(`repos/${options.repo}/issues/${number}/comments`);
@@ -383,6 +388,7 @@ async function pullRequests(client, options, previous, phase, progress) {
383
388
  current.diff = "";
384
389
  }
385
390
  }
391
+ cache[index] = current;
386
392
  if (!dropped)
387
393
  result[index] = current;
388
394
  completed++;
@@ -391,29 +397,82 @@ async function pullRequests(client, options, previous, phase, progress) {
391
397
  }
392
398
  log("fetch", `PR ${completed}/${selected.length} · ${percent(completed, selected.length)} #${number} · ${discussionStatus} · ${diffStatus}`);
393
399
  if (progress) {
394
- save = save.then(() => progress(result.filter((item) => !!item)));
400
+ save = save.then(() => progress(result.filter((item) => !!item), cache.filter((item) => !!item)));
395
401
  await save;
396
402
  }
397
403
  });
398
404
  const kept = result.filter((item) => !!item);
405
+ const decided = cache.filter((item) => !!item);
399
406
  if (options.onlyRequestChangedPr) {
400
407
  log("fetch", `only request-changed pr · kept ${kept.length} · dropped ${selected.length - kept.length}`);
401
408
  }
402
- return kept;
409
+ return { kept, cache: decided };
403
410
  }
404
- async function commits(client, options, meta, phase) {
411
+ function commitLimit(maxCommits) {
412
+ return maxCommits && maxCommits > 0 ? maxCommits : undefined;
413
+ }
414
+ function commitsCovered(cached, previousMax, limit) {
415
+ if (cached.length === 0)
416
+ return false;
417
+ const previousAll = commitLimit(previousMax) === undefined;
418
+ if (limit === undefined)
419
+ return previousAll;
420
+ return previousAll || cached.length >= limit;
421
+ }
422
+ async function commits(client, options, meta, previous, phase) {
405
423
  const branch = String(meta.default_branch ?? "main");
406
- const limit = options.maxCommits && options.maxCommits > 0
407
- ? options.maxCommits
408
- : undefined;
409
- if (phase)
410
- phase.text = "commit history · page 0 · fetched 0";
411
- return client.pages(`repos/${options.repo}/commits?sha=${encodeURIComponent(branch)}`, limit, (page, fetched) => {
424
+ const limit = commitLimit(options.maxCommits);
425
+ const cached = previous?.source.commits ?? [];
426
+ const cachedTip = String(cached[0]?.sha ?? "");
427
+ const endpoint = `repos/${options.repo}/commits?sha=${encodeURIComponent(branch)}`;
428
+ const note = (page, fetched) => {
412
429
  const message = `commit history · page ${page} · fetched ${fetched} · total unknown`;
413
430
  if (phase)
414
431
  phase.text = message;
415
432
  log("fetch", message);
416
- });
433
+ };
434
+ if (phase)
435
+ phase.text = "commit history · page 0 · fetched 0";
436
+ if (!cachedTip) {
437
+ return client.pages(endpoint, limit, note);
438
+ }
439
+ const fresh = [];
440
+ let found = false;
441
+ for (let page = 1;; page++) {
442
+ const pageItems = await client.request(`${endpoint}&per_page=100&page=${page}`);
443
+ const items = Array.isArray(pageItems) ? pageItems : [];
444
+ for (const commit of items) {
445
+ if (String(commit.sha ?? "") === cachedTip) {
446
+ found = true;
447
+ break;
448
+ }
449
+ fresh.push(commit);
450
+ if (limit !== undefined && fresh.length >= limit)
451
+ break;
452
+ }
453
+ note(page, fresh.length);
454
+ if (found ||
455
+ items.length < 100 ||
456
+ (limit !== undefined && fresh.length >= limit)) {
457
+ break;
458
+ }
459
+ }
460
+ if (!found || !commitsCovered(cached, previous?.options.maxCommits, limit)) {
461
+ if (found)
462
+ return client.pages(endpoint, limit, note);
463
+ return limit === undefined ? fresh : fresh.slice(0, limit);
464
+ }
465
+ const selected = [...fresh];
466
+ for (const commit of cached) {
467
+ if (limit !== undefined && selected.length >= limit)
468
+ break;
469
+ selected.push(commit);
470
+ }
471
+ const reused = selected.length - fresh.length;
472
+ if (reused > 0) {
473
+ log("fetch", `commit history · reused ${reused} cached commits · ${selected.length} total`);
474
+ }
475
+ return selected;
417
476
  }
418
477
  export async function collectSource(client, options, previous, progress, codebaseProgress) {
419
478
  const phase = { text: "reading repository metadata" };
@@ -426,16 +485,17 @@ export async function collectSource(client, options, previous, progress, codebas
426
485
  : { tree: [], treeSha: {}, files: {} };
427
486
  const pullRequestData = options.includePullRequests
428
487
  ? await pullRequests(client, options, previous, phase, progress)
429
- : [];
488
+ : { kept: [], cache: previous?.source.pullRequestCache ?? [] };
430
489
  const commitData = options.includeCommitHistory
431
- ? await commits(client, options, repo, phase)
490
+ ? await commits(client, options, repo, previous, phase)
432
491
  : [];
433
492
  return {
434
493
  repo,
435
494
  tree: files.tree,
436
495
  treeSha: files.treeSha,
437
496
  files: files.files,
438
- pullRequests: pullRequestData,
497
+ pullRequests: pullRequestData.kept,
498
+ pullRequestCache: pullRequestData.cache,
439
499
  commits: commitData,
440
500
  };
441
501
  }
@@ -23,7 +23,11 @@ export type Source = {
23
23
  tree: string[];
24
24
  treeSha: Record<string, string>;
25
25
  files: Record<string, string>;
26
+ /** Pull requests selected for facts and the skill. */
26
27
  pullRequests: PullRequest[];
28
+ /** Every pull request already decided, including ones left out of the skill.
29
+ * The next fetch reads this so a `changesRequested: false` result is not downloaded again. */
30
+ pullRequestCache?: PullRequest[];
27
31
  commits: Json[];
28
32
  };
29
33
  export type Fact = {
@@ -15,6 +15,7 @@ import { readState, writeState } from "../store/skill_state.js";
15
15
  import { cacheSet } from "../store/cache_db.js";
16
16
  import { log, timed, withLogSink } from "../util/log.js";
17
17
  import { enqueue, registerHandler } from "./jobs.js";
18
+ import { closeAppDb, isAppDbOpen, openAppDb } from "../store/app_db.js";
18
19
  import { getRepo, markKnowledgeBuilt } from "../store/repos.js";
19
20
  import { nowIso } from "../util/time.js";
20
21
  import { mkdir, readTextFile, remove } from "../util/runtime.js";
@@ -116,6 +117,18 @@ function addReviewLink(markdown, documents) {
116
117
  return `${markdown.trimEnd()}\n\n## Pull request review guides\n\n- Read [PR_REVIEW_GUIDE.md](PR_REVIEW_GUIDE.md).${detailedLink}\n`;
117
118
  }
118
119
  export async function runInitOrRemake(options) {
120
+ const openedAppDb = !isAppDbOpen();
121
+ if (openedAppDb)
122
+ await openAppDb();
123
+ try {
124
+ await initOrRemake(options);
125
+ }
126
+ finally {
127
+ if (openedAppDb)
128
+ await closeAppDb();
129
+ }
130
+ }
131
+ async function initOrRemake(options) {
119
132
  const operationStarted = performance.now();
120
133
  const aiMetrics = emptyAiMetrics();
121
134
  const previous = await readState(options.repo);
@@ -136,8 +149,9 @@ export async function runInitOrRemake(options) {
136
149
  log("ai", `${options.ai} providers configured · low=${options.lowModel} · high=${options.highModel}`);
137
150
  }
138
151
  log("fetch", `${options.repo} via ${options.auth}`);
139
- const source = await timed("fetch repository data", options.logTime, () => collectSource(client, options, previous, async (pullRequests) => {
152
+ const source = await timed("fetch repository data", options.logTime, () => collectSource(client, options, previous, async (pullRequests, cache) => {
140
153
  checkpoint.source.pullRequests = pullRequests;
154
+ checkpoint.source.pullRequestCache = cache;
141
155
  checkpoint.scanDone = {
142
156
  ...checkpoint.scanDone,
143
157
  pullRequests: pullRequests.length,
@@ -3,8 +3,10 @@ export declare function appDbDir(): string;
3
3
  /** `CM_APP_DB` overrides the path — tests point it at a temp file. */
4
4
  export declare function appDbPath(): string;
5
5
  export declare function releaseLock(): Promise<void>;
6
+ export declare function isAppDbOpen(): boolean;
6
7
  /** Opens (creating and migrating if needed) the single shared `app.db`
7
- * connection every store module reuses. Call once at `serve` startup. */
8
+ * connection every store module reuses. `serve` opens it for the process.
9
+ * `init` and `remake` open it for the run when it is not already open. */
8
10
  export declare function openAppDb(): Promise<Database>;
9
11
  export declare function getAppDb(): Database;
10
12
  export declare function closeAppDb(): Promise<void>;
@@ -59,8 +59,12 @@ function migrate(database) {
59
59
  }
60
60
  }
61
61
  let db;
62
+ export function isAppDbOpen() {
63
+ return db !== undefined;
64
+ }
62
65
  /** Opens (creating and migrating if needed) the single shared `app.db`
63
- * connection every store module reuses. Call once at `serve` startup. */
66
+ * connection every store module reuses. `serve` opens it for the process.
67
+ * `init` and `remake` open it for the run when it is not already open. */
64
68
  export async function openAppDb() {
65
69
  if (db)
66
70
  return db;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "co-maintainer",
3
- "version": "0.4.11",
3
+ "version": "0.4.13",
4
4
  "description": "Analyzes a GitHub repository and writes repository-specific contribution guidance.",
5
5
  "license": "MIT",
6
6
  "repository": {