co-maintainer 0.4.10 → 0.4.12
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 +1 -1
- package/dist/src/github/collect.d.ts +1 -1
- package/dist/src/github/collect.js +78 -18
- package/dist/src/knowledge/types.d.ts +4 -0
- package/dist/src/knowledge/validate.d.ts +1 -1
- package/dist/src/knowledge/validate.js +7 -69
- package/dist/src/services/setup.js +5 -4
- package/package.json +1 -1
package/dist/package.json
CHANGED
|
@@ -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
|
|
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
|
|
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 =
|
|
314
|
+
const discussionUnchanged = full?.updatedAt === current.updatedAt;
|
|
312
315
|
const decisionKnown = typeof cached?.changesRequested === "boolean";
|
|
313
|
-
const diffUnchanged =
|
|
314
|
-
let droppedFromCache = only &&
|
|
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
|
-
|
|
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
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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 = {
|
|
@@ -4,4 +4,4 @@ export type ValidationResult = {
|
|
|
4
4
|
errors: string[];
|
|
5
5
|
warnings: string[];
|
|
6
6
|
};
|
|
7
|
-
export declare function validateSkill(markdown: string,
|
|
7
|
+
export declare function validateSkill(markdown: string, source?: Source, referenceIssues?: "error" | "warning"): Promise<ValidationResult>;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export async function validateSkill(markdown, outputDirectory, source, referenceIssues = "error") {
|
|
1
|
+
export async function validateSkill(markdown, source, referenceIssues = "error") {
|
|
3
2
|
const errors = [];
|
|
4
3
|
const warnings = [];
|
|
5
4
|
const referenceProblems = referenceIssues === "error" ? errors : warnings;
|
|
@@ -33,54 +32,6 @@ export async function validateSkill(markdown, outputDirectory, source, reference
|
|
|
33
32
|
if (source) {
|
|
34
33
|
const paths = new Set([...source.tree, ...Object.keys(source.files)]);
|
|
35
34
|
const repositoryName = String(source.repo.full_name ?? "");
|
|
36
|
-
const commands = new Set();
|
|
37
|
-
for (const [path, content] of Object.entries(source.files)) {
|
|
38
|
-
if (path.endsWith("package.json")) {
|
|
39
|
-
try {
|
|
40
|
-
const scripts = JSON.parse(content)
|
|
41
|
-
.scripts ?? {};
|
|
42
|
-
const prefix = source.tree.some((item) => /pnpm-lock\.yaml$/.test(item))
|
|
43
|
-
? "pnpm"
|
|
44
|
-
: source.tree.some((item) => /yarn\.lock$/.test(item))
|
|
45
|
-
? "yarn"
|
|
46
|
-
: "npm";
|
|
47
|
-
for (const name of Object.keys(scripts)) {
|
|
48
|
-
commands.add(`${prefix} ${name}`);
|
|
49
|
-
commands.add(`${prefix} run ${name}`);
|
|
50
|
-
commands.add(String(scripts[name]));
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
// Ignore malformed manifests; syntax validation is outside this gate.
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (/deno\.jsonc?$/.test(path)) {
|
|
58
|
-
try {
|
|
59
|
-
const tasks = JSON.parse(content).tasks ??
|
|
60
|
-
{};
|
|
61
|
-
for (const [name, task] of Object.entries(tasks)) {
|
|
62
|
-
commands.add(`deno task ${name}`);
|
|
63
|
-
commands.add(String(task));
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
// Ignore malformed manifests; syntax validation is outside this gate.
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
for (const match of content.matchAll(/^\s*run:\s*([^\s#].*?)\s*$/gm)) {
|
|
71
|
-
commands.add(match[1].replace(/^['"]|['"]$/g, ""));
|
|
72
|
-
}
|
|
73
|
-
for (const match of content.matchAll(/`((?:npm|pnpm|yarn|bun|deno|cargo|make|go|python|node|git)\s+[^`\n]+)`/g)) {
|
|
74
|
-
commands.add(match[1]);
|
|
75
|
-
}
|
|
76
|
-
for (const match of content.matchAll(/^\s*((?:npm|pnpm|yarn|bun|deno|cargo|make|go|python|node|git)\s+[^\n`]+)$/gm)) {
|
|
77
|
-
commands.add(match[1].trim());
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (source.tree.some((path) => /Cargo\.toml$/.test(path))) {
|
|
81
|
-
commands.add("cargo test");
|
|
82
|
-
commands.add("cargo build");
|
|
83
|
-
}
|
|
84
35
|
const references = [...markdown.matchAll(/`([^`\n]+)`/g)].map((match) => match[1]);
|
|
85
36
|
for (const reference of references) {
|
|
86
37
|
const isQualifier = /^[a-z][a-z\d_-]*:/i.test(reference);
|
|
@@ -109,27 +60,14 @@ export async function validateSkill(markdown, outputDirectory, source, reference
|
|
|
109
60
|
referenceProblems.push(`referenced path is absent from source: ${reference}`);
|
|
110
61
|
}
|
|
111
62
|
}
|
|
112
|
-
if (/^(?:npm|pnpm|yarn|bun|deno|cargo|make|go|python|node|git)\s/.test(reference) &&
|
|
113
|
-
![...commands].some((command) => reference === command ||
|
|
114
|
-
reference.startsWith(`${command} `) ||
|
|
115
|
-
command.startsWith(`${reference} `))) {
|
|
116
|
-
referenceProblems.push(`referenced command is absent from source: ${reference}`);
|
|
117
|
-
}
|
|
118
63
|
}
|
|
119
64
|
}
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
if (/^[a-z][a-z\d+.-]*:/i.test(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
errors.push(`link is not relative: ${
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
try {
|
|
129
|
-
await stat(`${outputDirectory}/${link}`);
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
errors.push(`linked file does not exist: ${link}`);
|
|
65
|
+
for (const link of markdown.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)) {
|
|
66
|
+
const target = link[1];
|
|
67
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(target) ||
|
|
68
|
+
target.startsWith("/") ||
|
|
69
|
+
target.includes("..")) {
|
|
70
|
+
errors.push(`link is not relative: ${target}`);
|
|
133
71
|
}
|
|
134
72
|
}
|
|
135
73
|
return { valid: errors.length === 0, errors, warnings };
|
|
@@ -136,8 +136,9 @@ export async function runInitOrRemake(options) {
|
|
|
136
136
|
log("ai", `${options.ai} providers configured · low=${options.lowModel} · high=${options.highModel}`);
|
|
137
137
|
}
|
|
138
138
|
log("fetch", `${options.repo} via ${options.auth}`);
|
|
139
|
-
const source = await timed("fetch repository data", options.logTime, () => collectSource(client, options, previous, async (pullRequests) => {
|
|
139
|
+
const source = await timed("fetch repository data", options.logTime, () => collectSource(client, options, previous, async (pullRequests, cache) => {
|
|
140
140
|
checkpoint.source.pullRequests = pullRequests;
|
|
141
|
+
checkpoint.source.pullRequestCache = cache;
|
|
141
142
|
checkpoint.scanDone = {
|
|
142
143
|
...checkpoint.scanDone,
|
|
143
144
|
pullRequests: pullRequests.length,
|
|
@@ -204,19 +205,19 @@ export async function runInitOrRemake(options) {
|
|
|
204
205
|
let result = await timed("assemble skill", options.logTime, () => assembleSkill(options.repo, facts, previousMarkdown, previous?.sectionHashes ?? {}, overrides));
|
|
205
206
|
const reviewDocuments = buildReviewDocuments(facts);
|
|
206
207
|
result.markdown = addReviewLink(result.markdown, reviewDocuments);
|
|
207
|
-
const validation = await timed("validate skill", options.logTime, () => validateSkill(result.markdown,
|
|
208
|
+
const validation = await timed("validate skill", options.logTime, () => validateSkill(result.markdown, source));
|
|
208
209
|
if (!validation.valid && Object.keys(overrides).length) {
|
|
209
210
|
log("validate", `AI output rejected: ${validation.errors.join("; ")}`);
|
|
210
211
|
overrides = {};
|
|
211
212
|
result = await timed("reassemble valid skill", options.logTime, () => assembleSkill(options.repo, facts, previousMarkdown, previous?.sectionHashes ?? {}, overrides));
|
|
212
213
|
result.markdown = addReviewLink(result.markdown, reviewDocuments);
|
|
213
214
|
}
|
|
214
|
-
const finalValidation = await timed("final skill validation", options.logTime, () => validateSkill(result.markdown,
|
|
215
|
+
const finalValidation = await timed("final skill validation", options.logTime, () => validateSkill(result.markdown, source, "warning"));
|
|
215
216
|
if (finalValidation.warnings.length) {
|
|
216
217
|
log("validate", `stale references kept: ${finalValidation.warnings.join(", ")}`);
|
|
217
218
|
}
|
|
218
219
|
if (!finalValidation.valid) {
|
|
219
|
-
|
|
220
|
+
log("validate", `skill problems kept: ${finalValidation.errors.join("; ")}`);
|
|
220
221
|
}
|
|
221
222
|
await timed("write skill and state", options.logTime, async () => {
|
|
222
223
|
const { withKnowledgeLock } = await import("../review/guides.js");
|