co-maintainer 0.4.0-beta.1
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/LICENSE +21 -0
- package/README.md +105 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +9 -0
- package/dist/mod.d.ts +4 -0
- package/dist/mod.js +2 -0
- package/dist/package.json +54 -0
- package/dist/src/ai/batch.d.ts +16 -0
- package/dist/src/ai/batch.js +117 -0
- package/dist/src/ai/fake.d.ts +7 -0
- package/dist/src/ai/fake.js +40 -0
- package/dist/src/ai/hetzner.d.ts +16 -0
- package/dist/src/ai/hetzner.js +69 -0
- package/dist/src/ai/mermaid.d.ts +27 -0
- package/dist/src/ai/mermaid.js +457 -0
- package/dist/src/ai/mermaid_loop.d.ts +12 -0
- package/dist/src/ai/mermaid_loop.js +100 -0
- package/dist/src/ai/openrouter.d.ts +9 -0
- package/dist/src/ai/openrouter.js +29 -0
- package/dist/src/ai/provider.d.ts +4 -0
- package/dist/src/ai/provider.js +63 -0
- package/dist/src/cli/args.d.ts +5 -0
- package/dist/src/cli/args.js +252 -0
- package/dist/src/cli/commands/probe.d.ts +2 -0
- package/dist/src/cli/commands/probe.js +122 -0
- package/dist/src/cli/commands/review.d.ts +3 -0
- package/dist/src/cli/commands/review.js +84 -0
- package/dist/src/cli/commands/serve.d.ts +18 -0
- package/dist/src/cli/commands/serve.js +183 -0
- package/dist/src/cli/commands/set.d.ts +7 -0
- package/dist/src/cli/commands/set.js +171 -0
- package/dist/src/cli/main.d.ts +1 -0
- package/dist/src/cli/main.js +27 -0
- package/dist/src/cli/prompt.d.ts +4 -0
- package/dist/src/cli/prompt.js +26 -0
- package/dist/src/cli/review_args.d.ts +28 -0
- package/dist/src/cli/review_args.js +84 -0
- package/dist/src/cli/review_output.d.ts +2 -0
- package/dist/src/cli/review_output.js +10 -0
- package/dist/src/cli/review_result.d.ts +81 -0
- package/dist/src/cli/review_result.js +281 -0
- package/dist/src/config.d.ts +108 -0
- package/dist/src/config.js +138 -0
- package/dist/src/git/default_branch.d.ts +5 -0
- package/dist/src/git/default_branch.js +22 -0
- package/dist/src/github/app.d.ts +43 -0
- package/dist/src/github/app.js +120 -0
- package/dist/src/github/client.d.ts +17 -0
- package/dist/src/github/client.js +66 -0
- package/dist/src/github/collect.d.ts +7 -0
- package/dist/src/github/collect.js +368 -0
- package/dist/src/github/gh.d.ts +8 -0
- package/dist/src/github/gh.js +63 -0
- package/dist/src/github/jwt.d.ts +12 -0
- package/dist/src/github/jwt.js +84 -0
- package/dist/src/github/oauth.d.ts +9 -0
- package/dist/src/github/oauth.js +51 -0
- package/dist/src/github/pat.d.ts +10 -0
- package/dist/src/github/pat.js +60 -0
- package/dist/src/knowledge/facts.d.ts +3 -0
- package/dist/src/knowledge/facts.js +325 -0
- package/dist/src/knowledge/guide.d.ts +6 -0
- package/dist/src/knowledge/guide.js +82 -0
- package/dist/src/knowledge/probe.d.ts +12 -0
- package/dist/src/knowledge/probe.js +144 -0
- package/dist/src/knowledge/sections.d.ts +2 -0
- package/dist/src/knowledge/sections.js +13 -0
- package/dist/src/knowledge/skill.d.ts +10 -0
- package/dist/src/knowledge/skill.js +106 -0
- package/dist/src/knowledge/synthesis.d.ts +7 -0
- package/dist/src/knowledge/synthesis.js +315 -0
- package/dist/src/knowledge/types.d.ts +49 -0
- package/dist/src/knowledge/types.js +1 -0
- package/dist/src/knowledge/validate.d.ts +6 -0
- package/dist/src/knowledge/validate.js +134 -0
- package/dist/src/local/carry_over_store.d.ts +18 -0
- package/dist/src/local/carry_over_store.js +40 -0
- package/dist/src/local/codegraph_exclude.d.ts +3 -0
- package/dist/src/local/codegraph_exclude.js +26 -0
- package/dist/src/local/codegraph_prepare.d.ts +17 -0
- package/dist/src/local/codegraph_prepare.js +51 -0
- package/dist/src/local/git_ops.d.ts +12 -0
- package/dist/src/local/git_ops.js +98 -0
- package/dist/src/local/git_parse.d.ts +27 -0
- package/dist/src/local/git_parse.js +131 -0
- package/dist/src/local/git_revision.d.ts +15 -0
- package/dist/src/local/git_revision.js +141 -0
- package/dist/src/local/git_untracked.d.ts +21 -0
- package/dist/src/local/git_untracked.js +79 -0
- package/dist/src/local/review_local.d.ts +4 -0
- package/dist/src/local/review_local.js +273 -0
- package/dist/src/local/review_lock.d.ts +7 -0
- package/dist/src/local/review_lock.js +45 -0
- package/dist/src/pr/checkout.d.ts +25 -0
- package/dist/src/pr/checkout.js +136 -0
- package/dist/src/pr/codegraph_tool_args.d.ts +8 -0
- package/dist/src/pr/codegraph_tool_args.js +34 -0
- package/dist/src/pr/codegraph_tools.d.ts +15 -0
- package/dist/src/pr/codegraph_tools.js +408 -0
- package/dist/src/pr/diff_summary.d.ts +6 -0
- package/dist/src/pr/diff_summary.js +52 -0
- package/dist/src/pr/findings.d.ts +14 -0
- package/dist/src/pr/findings.js +94 -0
- package/dist/src/pr/github_revision.d.ts +4 -0
- package/dist/src/pr/github_revision.js +39 -0
- package/dist/src/pr/hunks.d.ts +18 -0
- package/dist/src/pr/hunks.js +73 -0
- package/dist/src/pr/reviewer.d.ts +40 -0
- package/dist/src/pr/reviewer.js +582 -0
- package/dist/src/pr/rounds.d.ts +17 -0
- package/dist/src/pr/rounds.js +43 -0
- package/dist/src/pr/scope.d.ts +35 -0
- package/dist/src/pr/scope.js +84 -0
- package/dist/src/pr/snapshot.d.ts +11 -0
- package/dist/src/pr/snapshot.js +1 -0
- package/dist/src/pr/suggestion.d.ts +17 -0
- package/dist/src/pr/suggestion.js +70 -0
- package/dist/src/remote/client.d.ts +4 -0
- package/dist/src/remote/client.js +199 -0
- package/dist/src/remote/client_tools.d.ts +13 -0
- package/dist/src/remote/client_tools.js +30 -0
- package/dist/src/remote/fixtures.d.ts +1 -0
- package/dist/src/remote/fixtures.js +30 -0
- package/dist/src/remote/revision_from_submit.d.ts +2 -0
- package/dist/src/remote/revision_from_submit.js +26 -0
- package/dist/src/remote/schema.d.ts +14 -0
- package/dist/src/remote/schema.js +30 -0
- package/dist/src/remote/server/body.d.ts +18 -0
- package/dist/src/remote/server/body.js +73 -0
- package/dist/src/remote/server/routes.d.ts +1 -0
- package/dist/src/remote/server/routes.js +150 -0
- package/dist/src/remote/server/sessions.d.ts +16 -0
- package/dist/src/remote/server/sessions.js +48 -0
- package/dist/src/remote/server/sync.d.ts +3 -0
- package/dist/src/remote/server/sync.js +90 -0
- package/dist/src/remote/tool_bridge.d.ts +15 -0
- package/dist/src/remote/tool_bridge.js +84 -0
- package/dist/src/remote/validate.d.ts +8 -0
- package/dist/src/remote/validate.js +248 -0
- package/dist/src/review/carry_over.d.ts +63 -0
- package/dist/src/review/carry_over.js +372 -0
- package/dist/src/review/engine.d.ts +13 -0
- package/dist/src/review/engine.js +4 -0
- package/dist/src/review/guides.d.ts +12 -0
- package/dist/src/review/guides.js +44 -0
- package/dist/src/review/revision.d.ts +26 -0
- package/dist/src/review/revision.js +57 -0
- package/dist/src/server/api/activity.d.ts +1 -0
- package/dist/src/server/api/activity.js +12 -0
- package/dist/src/server/api/analytics.d.ts +1 -0
- package/dist/src/server/api/analytics.js +9 -0
- package/dist/src/server/api/installations.d.ts +4 -0
- package/dist/src/server/api/installations.js +24 -0
- package/dist/src/server/api/jobs.d.ts +1 -0
- package/dist/src/server/api/jobs.js +86 -0
- package/dist/src/server/api/json_body.d.ts +1 -0
- package/dist/src/server/api/json_body.js +14 -0
- package/dist/src/server/api/remote_tokens.d.ts +1 -0
- package/dist/src/server/api/remote_tokens.js +71 -0
- package/dist/src/server/api/repos.d.ts +4 -0
- package/dist/src/server/api/repos.js +136 -0
- package/dist/src/server/api/settings.d.ts +1 -0
- package/dist/src/server/api/settings.js +210 -0
- package/dist/src/server/app.d.ts +27 -0
- package/dist/src/server/app.js +147 -0
- package/dist/src/server/assets.d.ts +3 -0
- package/dist/src/server/assets.js +21 -0
- package/dist/src/server/auth.d.ts +44 -0
- package/dist/src/server/auth.js +188 -0
- package/dist/src/server/errors.d.ts +8 -0
- package/dist/src/server/errors.js +10 -0
- package/dist/src/server/http.d.ts +8 -0
- package/dist/src/server/http.js +117 -0
- package/dist/src/server/logo.d.ts +1 -0
- package/dist/src/server/logo.js +2 -0
- package/dist/src/server/pages/activity.d.ts +4 -0
- package/dist/src/server/pages/activity.js +200 -0
- package/dist/src/server/pages/add_repo.d.ts +9 -0
- package/dist/src/server/pages/add_repo.js +56 -0
- package/dist/src/server/pages/analytics.d.ts +2 -0
- package/dist/src/server/pages/analytics.js +193 -0
- package/dist/src/server/pages/client.d.ts +2 -0
- package/dist/src/server/pages/client.js +2 -0
- package/dist/src/server/pages/home.d.ts +16 -0
- package/dist/src/server/pages/home.js +87 -0
- package/dist/src/server/pages/knowledge.d.ts +2 -0
- package/dist/src/server/pages/knowledge.js +64 -0
- package/dist/src/server/pages/layout.d.ts +32 -0
- package/dist/src/server/pages/layout.js +230 -0
- package/dist/src/server/pages/pr.d.ts +2 -0
- package/dist/src/server/pages/pr.js +109 -0
- package/dist/src/server/pages/repo.d.ts +2 -0
- package/dist/src/server/pages/repo.js +92 -0
- package/dist/src/server/pages/repo_prs.d.ts +2 -0
- package/dist/src/server/pages/repo_prs.js +75 -0
- package/dist/src/server/pages/repo_remote.d.ts +2 -0
- package/dist/src/server/pages/repo_remote.js +38 -0
- package/dist/src/server/pages/repo_settings.d.ts +3 -0
- package/dist/src/server/pages/repo_settings.js +127 -0
- package/dist/src/server/pages/router.d.ts +17 -0
- package/dist/src/server/pages/router.js +312 -0
- package/dist/src/server/pages/settings.d.ts +2 -0
- package/dist/src/server/pages/settings.js +337 -0
- package/dist/src/server/pages/setup.d.ts +5 -0
- package/dist/src/server/pages/setup.js +38 -0
- package/dist/src/server/pages/styles.d.ts +2 -0
- package/dist/src/server/pages/styles.js +2 -0
- package/dist/src/server/webhook/index.d.ts +1 -0
- package/dist/src/server/webhook/index.js +64 -0
- package/dist/src/server/webhook/signature.d.ts +7 -0
- package/dist/src/server/webhook/signature.js +41 -0
- package/dist/src/services/credentials.d.ts +19 -0
- package/dist/src/services/credentials.js +146 -0
- package/dist/src/services/dashboard.d.ts +165 -0
- package/dist/src/services/dashboard.js +200 -0
- package/dist/src/services/drift.d.ts +13 -0
- package/dist/src/services/drift.js +76 -0
- package/dist/src/services/jobs.d.ts +34 -0
- package/dist/src/services/jobs.js +222 -0
- package/dist/src/services/remote_review.d.ts +10 -0
- package/dist/src/services/remote_review.js +274 -0
- package/dist/src/services/remote_review_abort.d.ts +3 -0
- package/dist/src/services/remote_review_abort.js +11 -0
- package/dist/src/services/remote_token_jobs.d.ts +1 -0
- package/dist/src/services/remote_token_jobs.js +20 -0
- package/dist/src/services/remote_tokens.d.ts +13 -0
- package/dist/src/services/remote_tokens.js +57 -0
- package/dist/src/services/replies.d.ts +7 -0
- package/dist/src/services/replies.js +248 -0
- package/dist/src/services/review.d.ts +35 -0
- package/dist/src/services/review.js +671 -0
- package/dist/src/services/setup.d.ts +29 -0
- package/dist/src/services/setup.js +364 -0
- package/dist/src/services/webhook.d.ts +22 -0
- package/dist/src/services/webhook.js +401 -0
- package/dist/src/store/app_db.d.ts +10 -0
- package/dist/src/store/app_db.js +85 -0
- package/dist/src/store/cache_db.d.ts +4 -0
- package/dist/src/store/cache_db.js +63 -0
- package/dist/src/store/deliveries.d.ts +12 -0
- package/dist/src/store/deliveries.js +23 -0
- package/dist/src/store/drift.d.ts +4 -0
- package/dist/src/store/drift.js +22 -0
- package/dist/src/store/findings.d.ts +31 -0
- package/dist/src/store/findings.js +64 -0
- package/dist/src/store/installations.d.ts +9 -0
- package/dist/src/store/installations.js +35 -0
- package/dist/src/store/job_logs.d.ts +5 -0
- package/dist/src/store/job_logs.js +17 -0
- package/dist/src/store/jobs.d.ts +27 -0
- package/dist/src/store/jobs.js +86 -0
- package/dist/src/store/migrations.d.ts +5 -0
- package/dist/src/store/migrations.js +258 -0
- package/dist/src/store/remote_review_inputs.d.ts +11 -0
- package/dist/src/store/remote_review_inputs.js +25 -0
- package/dist/src/store/remote_tokens.d.ts +10 -0
- package/dist/src/store/remote_tokens.js +45 -0
- package/dist/src/store/replies.d.ts +24 -0
- package/dist/src/store/replies.js +79 -0
- package/dist/src/store/repos.d.ts +11 -0
- package/dist/src/store/repos.js +59 -0
- package/dist/src/store/reviews.d.ts +86 -0
- package/dist/src/store/reviews.js +220 -0
- package/dist/src/store/rows.d.ts +179 -0
- package/dist/src/store/rows.js +4 -0
- package/dist/src/store/sessions.d.ts +8 -0
- package/dist/src/store/sessions.js +34 -0
- package/dist/src/store/skill_state.d.ts +3 -0
- package/dist/src/store/skill_state.js +24 -0
- package/dist/src/store/sqlite.d.ts +39 -0
- package/dist/src/store/sqlite.js +77 -0
- package/dist/src/store/subjects.d.ts +15 -0
- package/dist/src/store/subjects.js +78 -0
- package/dist/src/tools/codegraph.d.ts +58 -0
- package/dist/src/tools/codegraph.js +165 -0
- package/dist/src/tools/codegraph_exec.d.ts +13 -0
- package/dist/src/tools/codegraph_exec.js +66 -0
- package/dist/src/types.d.ts +89 -0
- package/dist/src/types.js +1 -0
- package/dist/src/util/atomic.d.ts +4 -0
- package/dist/src/util/atomic.js +78 -0
- package/dist/src/util/keyed_lock.d.ts +3 -0
- package/dist/src/util/keyed_lock.js +33 -0
- package/dist/src/util/log.d.ts +11 -0
- package/dist/src/util/log.js +42 -0
- package/dist/src/util/redact.d.ts +5 -0
- package/dist/src/util/redact.js +30 -0
- package/dist/src/util/runtime.d.ts +90 -0
- package/dist/src/util/runtime.js +206 -0
- package/dist/src/util/time.d.ts +9 -0
- package/dist/src/util/time.js +36 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +5 -0
- package/package.json +54 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** GitHub hands out App private keys as PKCS#1 PEM ("BEGIN RSA PRIVATE
|
|
2
|
+
* KEY"). Web Crypto's `importKey("pkcs8", ...)` only takes PKCS#8. The two
|
|
3
|
+
* differ by a fixed 26-byte wrapper around the same PKCS#1 bytes: a version
|
|
4
|
+
* INTEGER, the rsaEncryption AlgorithmIdentifier, and an OCTET STRING tag
|
|
5
|
+
* around the PKCS#1 payload — this file builds that wrapper by hand rather
|
|
6
|
+
* than pulling in an ASN.1 library for one fixed structure. */
|
|
7
|
+
const RSA_ALGORITHM_IDENTIFIER = Uint8Array.from([
|
|
8
|
+
0x30,
|
|
9
|
+
0x0d, // SEQUENCE, 13 bytes
|
|
10
|
+
0x06,
|
|
11
|
+
0x09,
|
|
12
|
+
0x2a,
|
|
13
|
+
0x86,
|
|
14
|
+
0x48,
|
|
15
|
+
0x86,
|
|
16
|
+
0xf7,
|
|
17
|
+
0x0d,
|
|
18
|
+
0x01,
|
|
19
|
+
0x01,
|
|
20
|
+
0x01, // OID 1.2.840.113549.1.1.1 rsaEncryption
|
|
21
|
+
0x05,
|
|
22
|
+
0x00, // NULL
|
|
23
|
+
]);
|
|
24
|
+
function derLength(length) {
|
|
25
|
+
if (length < 0x80)
|
|
26
|
+
return [length];
|
|
27
|
+
const bytes = [];
|
|
28
|
+
for (let n = length; n > 0; n >>= 8)
|
|
29
|
+
bytes.unshift(n & 0xff);
|
|
30
|
+
return [0x80 | bytes.length, ...bytes];
|
|
31
|
+
}
|
|
32
|
+
function derTlv(tag, content) {
|
|
33
|
+
return Uint8Array.from([tag, ...derLength(content.length), ...content]);
|
|
34
|
+
}
|
|
35
|
+
export function pkcs1ToPkcs8(pkcs1) {
|
|
36
|
+
const version = Uint8Array.from([0x02, 0x01, 0x00]);
|
|
37
|
+
const octetString = derTlv(0x04, pkcs1);
|
|
38
|
+
const inner = new Uint8Array(version.length + RSA_ALGORITHM_IDENTIFIER.length + octetString.length);
|
|
39
|
+
inner.set(version, 0);
|
|
40
|
+
inner.set(RSA_ALGORITHM_IDENTIFIER, version.length);
|
|
41
|
+
inner.set(octetString, version.length + RSA_ALGORITHM_IDENTIFIER.length);
|
|
42
|
+
return derTlv(0x30, inner);
|
|
43
|
+
}
|
|
44
|
+
function pemToDer(pem) {
|
|
45
|
+
const match = pem.match(/-----BEGIN ([^-]+)-----([\s\S]+?)-----END \1-----/);
|
|
46
|
+
if (!match)
|
|
47
|
+
throw new Error("not a PEM-encoded key");
|
|
48
|
+
const base64 = match[2].replace(/\s+/g, "");
|
|
49
|
+
const der = Uint8Array.from(atob(base64), (char) => char.charCodeAt(0));
|
|
50
|
+
return { der, label: match[1].trim() };
|
|
51
|
+
}
|
|
52
|
+
export async function importAppPrivateKey(pem) {
|
|
53
|
+
const { der, label } = pemToDer(pem);
|
|
54
|
+
let pkcs8;
|
|
55
|
+
if (label === "RSA PRIVATE KEY")
|
|
56
|
+
pkcs8 = pkcs1ToPkcs8(der);
|
|
57
|
+
else if (label === "PRIVATE KEY")
|
|
58
|
+
pkcs8 = der;
|
|
59
|
+
else
|
|
60
|
+
throw new Error(`unsupported private key PEM label: ${label}`);
|
|
61
|
+
return await crypto.subtle.importKey("pkcs8", pkcs8, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["sign"]);
|
|
62
|
+
}
|
|
63
|
+
function base64url(bytes) {
|
|
64
|
+
return btoa(String.fromCharCode(...bytes))
|
|
65
|
+
.replace(/\+/g, "-")
|
|
66
|
+
.replace(/\//g, "_")
|
|
67
|
+
.replace(/=+$/, "");
|
|
68
|
+
}
|
|
69
|
+
function base64urlJson(value) {
|
|
70
|
+
return base64url(new TextEncoder().encode(JSON.stringify(value)));
|
|
71
|
+
}
|
|
72
|
+
/** GitHub requires `exp` no more than 10 minutes out and tolerates `iat` a
|
|
73
|
+
* little in the past to absorb clock drift between this machine and
|
|
74
|
+
* GitHub's. */
|
|
75
|
+
export async function signAppJwt(appId, key) {
|
|
76
|
+
const now = Math.floor(Date.now() / 1000);
|
|
77
|
+
const signingInput = `${base64urlJson({ alg: "RS256", typ: "JWT" })}.${base64urlJson({
|
|
78
|
+
iat: now - 60,
|
|
79
|
+
exp: now + 600,
|
|
80
|
+
iss: appId,
|
|
81
|
+
})}`;
|
|
82
|
+
const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, new TextEncoder().encode(signingInput));
|
|
83
|
+
return `${signingInput}.${base64url(new Uint8Array(signature))}`;
|
|
84
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function githubAuthorizeUrl(clientId: string, redirectUri: string, state: string): string;
|
|
2
|
+
/** Exchanges an authorization `code` for the GitHub login it belongs to,
|
|
3
|
+
* or `undefined` if the exchange or the `/user` lookup fails. */
|
|
4
|
+
export declare function githubLoginFromCode(opts: {
|
|
5
|
+
clientId: string;
|
|
6
|
+
clientSecret: string;
|
|
7
|
+
code: string;
|
|
8
|
+
redirectUri: string;
|
|
9
|
+
}): Promise<string | undefined>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** The user-to-server side of a GitHub App's OAuth flow — "Sign in with
|
|
2
|
+
* GitHub" — reusing the App's own client ID/secret. No separate OAuth App
|
|
3
|
+
* is needed. */
|
|
4
|
+
import { githubFetch } from "./client.js";
|
|
5
|
+
export function githubAuthorizeUrl(clientId, redirectUri, state) {
|
|
6
|
+
const url = new URL("https://github.com/login/oauth/authorize");
|
|
7
|
+
url.searchParams.set("client_id", clientId);
|
|
8
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
9
|
+
url.searchParams.set("state", state);
|
|
10
|
+
return url.toString();
|
|
11
|
+
}
|
|
12
|
+
async function exchangeGithubCode(opts) {
|
|
13
|
+
const response = await githubFetch("https://github.com/login/oauth/access_token", {
|
|
14
|
+
method: "POST",
|
|
15
|
+
headers: {
|
|
16
|
+
"content-type": "application/json",
|
|
17
|
+
accept: "application/json",
|
|
18
|
+
},
|
|
19
|
+
body: JSON.stringify({
|
|
20
|
+
client_id: opts.clientId,
|
|
21
|
+
client_secret: opts.clientSecret,
|
|
22
|
+
code: opts.code,
|
|
23
|
+
redirect_uri: opts.redirectUri,
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok)
|
|
27
|
+
return undefined;
|
|
28
|
+
const body = (await response.json());
|
|
29
|
+
return body.access_token;
|
|
30
|
+
}
|
|
31
|
+
async function githubLoginFor(accessToken) {
|
|
32
|
+
const response = await githubFetch("https://api.github.com/user", {
|
|
33
|
+
headers: {
|
|
34
|
+
Accept: "application/vnd.github+json",
|
|
35
|
+
Authorization: `Bearer ${accessToken}`,
|
|
36
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
if (!response.ok)
|
|
40
|
+
return undefined;
|
|
41
|
+
const body = (await response.json());
|
|
42
|
+
return body.login;
|
|
43
|
+
}
|
|
44
|
+
/** Exchanges an authorization `code` for the GitHub login it belongs to,
|
|
45
|
+
* or `undefined` if the exchange or the `/user` lookup fails. */
|
|
46
|
+
export async function githubLoginFromCode(opts) {
|
|
47
|
+
const accessToken = await exchangeGithubCode(opts);
|
|
48
|
+
if (!accessToken)
|
|
49
|
+
return undefined;
|
|
50
|
+
return await githubLoginFor(accessToken);
|
|
51
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { GitHubClient } from "../types.ts";
|
|
2
|
+
export declare class PatClient implements GitHubClient {
|
|
3
|
+
private readonly token;
|
|
4
|
+
constructor(token: string);
|
|
5
|
+
request<T>(endpoint: string): Promise<T>;
|
|
6
|
+
write<T>(endpoint: string, body: unknown): Promise<T>;
|
|
7
|
+
createCheckRun<T>(endpoint: string, body: unknown): Promise<T>;
|
|
8
|
+
updateCheckRun<T>(endpoint: string, body: unknown): Promise<T>;
|
|
9
|
+
pages<T>(endpoint: string, limit?: number, progress?: (page: number, fetched: number) => void): Promise<T[]>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { githubFetch, GitHubHttpError, paginate } from "./client.js";
|
|
2
|
+
export class PatClient {
|
|
3
|
+
token;
|
|
4
|
+
constructor(token) {
|
|
5
|
+
if (!token)
|
|
6
|
+
throw new Error("PAT mode requires GITHUB_TOKEN or GH_TOKEN");
|
|
7
|
+
this.token = token;
|
|
8
|
+
}
|
|
9
|
+
async request(endpoint) {
|
|
10
|
+
const response = await githubFetch(`https://api.github.com/${endpoint}`, {
|
|
11
|
+
headers: {
|
|
12
|
+
Accept: "application/vnd.github+json",
|
|
13
|
+
Authorization: `Bearer ${this.token}`,
|
|
14
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
throw new GitHubHttpError(response.status, await response.text());
|
|
19
|
+
}
|
|
20
|
+
return (await response.json());
|
|
21
|
+
}
|
|
22
|
+
async write(endpoint, body) {
|
|
23
|
+
const response = await githubFetch(`https://api.github.com/${endpoint}`, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: {
|
|
26
|
+
Accept: "application/vnd.github+json",
|
|
27
|
+
Authorization: `Bearer ${this.token}`,
|
|
28
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
29
|
+
"content-type": "application/json",
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify(body),
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new GitHubHttpError(response.status, await response.text());
|
|
35
|
+
}
|
|
36
|
+
return (await response.json());
|
|
37
|
+
}
|
|
38
|
+
async createCheckRun(endpoint, body) {
|
|
39
|
+
return await this.write(endpoint, body);
|
|
40
|
+
}
|
|
41
|
+
async updateCheckRun(endpoint, body) {
|
|
42
|
+
const response = await githubFetch(`https://api.github.com/${endpoint}`, {
|
|
43
|
+
method: "PATCH",
|
|
44
|
+
headers: {
|
|
45
|
+
Accept: "application/vnd.github+json",
|
|
46
|
+
Authorization: `Bearer ${this.token}`,
|
|
47
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
48
|
+
"content-type": "application/json",
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify(body),
|
|
51
|
+
});
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
throw new GitHubHttpError(response.status, await response.text());
|
|
54
|
+
}
|
|
55
|
+
return (await response.json());
|
|
56
|
+
}
|
|
57
|
+
pages(endpoint, limit, progress) {
|
|
58
|
+
return paginate((e) => this.request(e), endpoint, limit, progress);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
function fact(sectionKey, claim, evidence, weight = 1) {
|
|
2
|
+
return {
|
|
3
|
+
id: `${sectionKey}:${claim.toLowerCase().replaceAll(/\W+/g, "-")}`,
|
|
4
|
+
sectionKey,
|
|
5
|
+
claim,
|
|
6
|
+
evidence: [evidence],
|
|
7
|
+
weight,
|
|
8
|
+
scope: "current",
|
|
9
|
+
confidence: "high",
|
|
10
|
+
status: "active",
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function add(facts, item) {
|
|
14
|
+
const existing = facts.get(item.id);
|
|
15
|
+
if (!existing)
|
|
16
|
+
facts.set(item.id, item);
|
|
17
|
+
else {
|
|
18
|
+
existing.weight += item.weight;
|
|
19
|
+
existing.evidence = [...new Set([...existing.evidence, ...item.evidence])];
|
|
20
|
+
if (existing.scope !== item.scope) {
|
|
21
|
+
existing.scope =
|
|
22
|
+
existing.scope === "current" || item.scope === "current"
|
|
23
|
+
? "current"
|
|
24
|
+
: "repeated-history";
|
|
25
|
+
}
|
|
26
|
+
existing.confidence =
|
|
27
|
+
existing.confidence === "high" || item.confidence === "high"
|
|
28
|
+
? "high"
|
|
29
|
+
: existing.confidence === "medium" || item.confidence === "medium"
|
|
30
|
+
? "medium"
|
|
31
|
+
: "low";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function counts(values) {
|
|
35
|
+
const result = new Map();
|
|
36
|
+
for (const value of values.filter(Boolean)) {
|
|
37
|
+
result.set(value, (result.get(value) ?? 0) + 1);
|
|
38
|
+
}
|
|
39
|
+
return [...result.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
40
|
+
}
|
|
41
|
+
function packageScripts(content) {
|
|
42
|
+
try {
|
|
43
|
+
const scripts = JSON.parse(content).scripts;
|
|
44
|
+
return scripts
|
|
45
|
+
? Object.entries(scripts).map(([name, command]) => [
|
|
46
|
+
name,
|
|
47
|
+
String(command),
|
|
48
|
+
])
|
|
49
|
+
: [];
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function markdownSections(content) {
|
|
56
|
+
const sections = [];
|
|
57
|
+
let heading = "";
|
|
58
|
+
let lines = [];
|
|
59
|
+
const flush = () => {
|
|
60
|
+
const summary = lines
|
|
61
|
+
.join(" ")
|
|
62
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
63
|
+
.replace(/https?:\/\/\S+/g, "")
|
|
64
|
+
.replace(/\s+/g, " ")
|
|
65
|
+
.trim();
|
|
66
|
+
if (heading && summary) {
|
|
67
|
+
sections.push([heading, summary]);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
for (const line of content.split(/\r?\n/)) {
|
|
71
|
+
const match = line.match(/^#{1,3}\s+(.+)$/);
|
|
72
|
+
if (match) {
|
|
73
|
+
flush();
|
|
74
|
+
heading = match[1].trim();
|
|
75
|
+
lines = [];
|
|
76
|
+
}
|
|
77
|
+
else if (heading && line.trim() && !line.trim().startsWith("```")) {
|
|
78
|
+
lines.push(line.trim());
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
flush();
|
|
82
|
+
return sections;
|
|
83
|
+
}
|
|
84
|
+
function cargoPackage(content) {
|
|
85
|
+
return content.match(/^\s*name\s*=\s*"([^"]+)"/m)?.[1];
|
|
86
|
+
}
|
|
87
|
+
function sourceModuleClaim(path, content) {
|
|
88
|
+
const symbols = [
|
|
89
|
+
...content.matchAll(/\b(?:pub\s+)?(?:struct|enum|trait|fn|function|class|interface|type|func|def|const)\s+([A-Za-z_]\w*)|\bexport\s+(?:default\s+)?(?:function|class|const|interface|type)\s+([A-Za-z_]\w*)/g),
|
|
90
|
+
]
|
|
91
|
+
.map((match) => match[1] ?? match[2])
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.slice(0, 8);
|
|
94
|
+
if (!symbols.length)
|
|
95
|
+
return undefined;
|
|
96
|
+
return `\`${path}\` contains ${symbols
|
|
97
|
+
.map((symbol) => `\`${symbol}\``)
|
|
98
|
+
.join(", ")}; keep related changes in this module.`;
|
|
99
|
+
}
|
|
100
|
+
function commandPrefix(source) {
|
|
101
|
+
if (source.tree.some((path) => /pnpm-lock\.yaml$/.test(path)))
|
|
102
|
+
return "pnpm";
|
|
103
|
+
if (source.tree.some((path) => /yarn\.lock$/.test(path)))
|
|
104
|
+
return "yarn";
|
|
105
|
+
if (source.tree.some((path) => /bun\.lockb?$/.test(path)))
|
|
106
|
+
return "bun";
|
|
107
|
+
if (source.tree.some((path) => /deno\.json(?:c)?$/.test(path))) {
|
|
108
|
+
return "deno task";
|
|
109
|
+
}
|
|
110
|
+
if (source.tree.some((path) => /Cargo\.toml$/.test(path)))
|
|
111
|
+
return "cargo";
|
|
112
|
+
if (source.tree.some((path) => /Makefile$|makefile$/i.test(path))) {
|
|
113
|
+
return "make";
|
|
114
|
+
}
|
|
115
|
+
return "npm run";
|
|
116
|
+
}
|
|
117
|
+
function sanitizeDocumentBody(body, source) {
|
|
118
|
+
const paths = new Set([...source.tree, ...Object.keys(source.files)]);
|
|
119
|
+
const repositoryName = String(source.repo.full_name ?? "");
|
|
120
|
+
const known = (reference) => {
|
|
121
|
+
if (reference.startsWith("@") || reference === repositoryName)
|
|
122
|
+
return true;
|
|
123
|
+
const normalized = reference.replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
124
|
+
const wildcard = normalized.includes("*")
|
|
125
|
+
? new RegExp(`^${normalized
|
|
126
|
+
.split("*")
|
|
127
|
+
.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
128
|
+
.join(".*")}$`)
|
|
129
|
+
: undefined;
|
|
130
|
+
return (paths.has(normalized) ||
|
|
131
|
+
[...paths].some((path) => wildcard ? wildcard.test(path) : path.startsWith(`${normalized}/`)));
|
|
132
|
+
};
|
|
133
|
+
return body
|
|
134
|
+
.replace(/`([^`\n]+\/[^`\n]+)`/g, (whole, reference) => known(reference) ? whole : "")
|
|
135
|
+
.replace(/(?:^|[\s("'`])((?!@)[A-Za-z0-9_.-]+\/[A-Za-z0-9_.*?{}<>:+-]+(?:\/[A-Za-z0-9_.*?{}<>:+-]+)*)/g, (whole, reference) => known(reference) ? whole : whole.replace(reference, ""));
|
|
136
|
+
}
|
|
137
|
+
export function extractFacts(source, options) {
|
|
138
|
+
const facts = new Map();
|
|
139
|
+
const fullName = String(source.repo.full_name ?? "");
|
|
140
|
+
const branch = String(source.repo.default_branch ?? "main");
|
|
141
|
+
add(facts, fact("identity", `Work in ${fullName} on the \`${branch}\` default branch.`, "repository metadata", 3));
|
|
142
|
+
const directories = [
|
|
143
|
+
...new Set(source.tree
|
|
144
|
+
.map((path) => path.split("/")[0])
|
|
145
|
+
.filter((path) => path &&
|
|
146
|
+
!path.includes(".") &&
|
|
147
|
+
!/^(test|tests|spec|__tests__)$/i.test(path))),
|
|
148
|
+
].sort();
|
|
149
|
+
if (directories.length) {
|
|
150
|
+
add(facts, fact("layout", `Top-level directories include ${directories
|
|
151
|
+
.slice(0, 12)
|
|
152
|
+
.map((dir) => `\`${dir}/\``)
|
|
153
|
+
.join(", ")}.`, "repository tree", 2));
|
|
154
|
+
}
|
|
155
|
+
const testRoots = [
|
|
156
|
+
...new Set(source.tree
|
|
157
|
+
.map((path) => path.split("/")[0])
|
|
158
|
+
.filter((root) => /^(test|tests|spec|__tests__)$/i.test(root))),
|
|
159
|
+
];
|
|
160
|
+
if (testRoots.length) {
|
|
161
|
+
add(facts, fact("tests", `Repository tests live under ${testRoots.map((root) => `\`${root}/\``).join(", ")}.`, "repository tree", 3));
|
|
162
|
+
}
|
|
163
|
+
const workflows = source.tree.filter((path) => /^\.github\/workflows\/.+\.(yml|yaml)$/i.test(path));
|
|
164
|
+
if (workflows.length) {
|
|
165
|
+
add(facts, fact("ship", `CI workflows are defined in ${workflows.map((path) => `\`${path}\``).join(", ")}.`, "repository tree", 3));
|
|
166
|
+
}
|
|
167
|
+
if (source.tree.some((path) => /^\.github\/workflows\/.+release.+\.(yml|yaml)$/i.test(path) ||
|
|
168
|
+
/(^|\/)(changesets|release-please|\.releaserc)/i.test(path))) {
|
|
169
|
+
add(facts, fact("ship", "The repository contains an explicit release workflow or release configuration.", "release files", 3));
|
|
170
|
+
}
|
|
171
|
+
const seenScripts = new Set();
|
|
172
|
+
for (const [path, content] of Object.entries(source.files)) {
|
|
173
|
+
// Anchored at any path segment, not just the repository root: a Cargo
|
|
174
|
+
// workspace nests each crate's source under `<crate>/src/...`, and this
|
|
175
|
+
// very repository's own `packages/<pkg>/src/...` layout is the same
|
|
176
|
+
// shape. A root-only anchor silently produced zero layout facts for both.
|
|
177
|
+
if (/(?:^|\/)(?:src|lib|app|cmd)\//i.test(path)) {
|
|
178
|
+
const claim = sourceModuleClaim(path, content);
|
|
179
|
+
if (claim)
|
|
180
|
+
add(facts, fact("layout", claim, path, 3));
|
|
181
|
+
}
|
|
182
|
+
if (/^README\.md$/i.test(path) || /CONTRIBUTING/i.test(path)) {
|
|
183
|
+
for (const [heading, body] of markdownSections(content)) {
|
|
184
|
+
const isReadme = /^README\.md$/i.test(path);
|
|
185
|
+
const relevant = isReadme
|
|
186
|
+
? /usage|run|develop|build|test|contribut|release/i.test(heading)
|
|
187
|
+
: /install|setup|usage|run|develop|build|test|contribut|release/i.test(heading);
|
|
188
|
+
if (!relevant) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
add(facts, fact("devloop", `README ${heading}: ${sanitizeDocumentBody(body, source).slice(0, 260)}${body.length > 260 ? "…" : ""}`, path, 3));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (/^Cargo\.toml$/i.test(path)) {
|
|
195
|
+
const packageName = cargoPackage(content);
|
|
196
|
+
add(facts, fact("identity", `This is a Rust Cargo project${packageName ? ` named \`${packageName}\`` : ""}.`, path, 4));
|
|
197
|
+
add(facts, fact("tests", "Run the Rust test suite with `cargo test`.", path, 3));
|
|
198
|
+
}
|
|
199
|
+
if (/^CHANGELOG\.md$/i.test(path)) {
|
|
200
|
+
add(facts, fact("ship", "Release notes are maintained in `CHANGELOG.md`.", path, 3));
|
|
201
|
+
}
|
|
202
|
+
if (/PULL_REQUEST_TEMPLATE/i.test(path)) {
|
|
203
|
+
add(facts, fact("title-body", `Use the pull request template at \`${path}\`.`, path, 4));
|
|
204
|
+
const headings = [...content.matchAll(/^#{1,3}\s+(.+)$/gm)]
|
|
205
|
+
.map((match) => match[1].trim())
|
|
206
|
+
.slice(0, 12);
|
|
207
|
+
if (headings.length) {
|
|
208
|
+
add(facts, fact("title-body", `The pull request template asks for: ${headings
|
|
209
|
+
.map((heading) => `**${heading}**`)
|
|
210
|
+
.join(", ")}.`, path, 4));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const [name, command] of path.endsWith("package.json")
|
|
214
|
+
? packageScripts(content)
|
|
215
|
+
: []) {
|
|
216
|
+
const rootScript = path === "package.json";
|
|
217
|
+
const usefulWorkspaceScript = /test|lint|check|type|build|load|compare/i.test(name);
|
|
218
|
+
if (!rootScript && !usefulWorkspaceScript)
|
|
219
|
+
continue;
|
|
220
|
+
const cleanCommand = sanitizeDocumentBody(command, source)
|
|
221
|
+
.replace(/\s+/g, " ")
|
|
222
|
+
.trim();
|
|
223
|
+
if (!cleanCommand)
|
|
224
|
+
continue;
|
|
225
|
+
const scriptKey = `${name}:${command}`;
|
|
226
|
+
if (seenScripts.has(scriptKey))
|
|
227
|
+
continue;
|
|
228
|
+
seenScripts.add(scriptKey);
|
|
229
|
+
const section = /test|lint|check|type/i.test(name)
|
|
230
|
+
? "tests"
|
|
231
|
+
: /build|dev|start|format/i.test(name)
|
|
232
|
+
? "devloop"
|
|
233
|
+
: "ship";
|
|
234
|
+
add(facts, fact(section, `Use \`${commandPrefix(source)} ${name}\` for ${cleanCommand}.`, path, 2));
|
|
235
|
+
}
|
|
236
|
+
if (/^\.github\/workflows\//.test(path)) {
|
|
237
|
+
const jobsBlock = content.match(/^jobs:\s*\n([\s\S]*?)(?=^[^\s]|\s*$)/m)?.[1] ?? "";
|
|
238
|
+
const jobs = [...jobsBlock.matchAll(/^\s{2}([A-Za-z0-9_-]+):\s*$/gm)]
|
|
239
|
+
.map((match) => match[1])
|
|
240
|
+
.slice(0, 20);
|
|
241
|
+
if (jobs.length) {
|
|
242
|
+
add(facts, fact("ship", `Workflow \`${path}\` defines jobs: ${jobs.map((job) => `\`${job}\``).join(", ")}.`, path, 2));
|
|
243
|
+
}
|
|
244
|
+
if (/\bon:\s*[\s\S]{0,200}\bpull_request\b/.test(content)) {
|
|
245
|
+
add(facts, fact("ship", `Changes targeting pull requests run \`${path}\`.`, path, 3));
|
|
246
|
+
}
|
|
247
|
+
if (/\bpush:\s*\n\s+branches:\s*\n\s+-\s*main\b/.test(content)) {
|
|
248
|
+
add(facts, fact("ship", `The release workflow runs when \`main\` receives a push.`, path, 4));
|
|
249
|
+
}
|
|
250
|
+
if (/runs-on:\s*windows-latest/i.test(content)) {
|
|
251
|
+
add(facts, fact("ship", `Release builds run on \`windows-latest\`.`, path, 3));
|
|
252
|
+
}
|
|
253
|
+
if (/cargo\s+build\s+--release/.test(content)) {
|
|
254
|
+
add(facts, fact("ship", "The release pipeline builds the Rust binary with `cargo build --release`.", path, 4));
|
|
255
|
+
}
|
|
256
|
+
if (/gh\s+release\s+create/.test(content)) {
|
|
257
|
+
add(facts, fact("ship", "Publishing uses `gh release create` with a generated version tag and `CHANGELOG.md` notes.", path, 4));
|
|
258
|
+
}
|
|
259
|
+
if (/Cargo\.toml/.test(content) && /version\s*=/i.test(content)) {
|
|
260
|
+
add(facts, fact("ship", "The release tag is derived from the version in `Cargo.toml`.", path, 4));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (/CONTRIBUTING/i.test(path)) {
|
|
264
|
+
add(facts, fact("devloop", `Follow repository contribution guidance in \`${path}\`.`, path, 4));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (source.pullRequests.length) {
|
|
268
|
+
const merged = source.pullRequests.filter((pr) => pr.merged).length;
|
|
269
|
+
add(facts, fact("process", `${merged}/${source.pullRequests.length} selected pull requests are merged; keep work mergeable before requesting review.`, "pull request metadata", 2));
|
|
270
|
+
const labels = counts(source.pullRequests.flatMap((pr) => pr.labels))
|
|
271
|
+
.slice(0, 12)
|
|
272
|
+
.map(([label, count]) => `\`${label}\` (${count})`);
|
|
273
|
+
if (labels.length) {
|
|
274
|
+
add(facts, fact("labels", `Common pull request labels are ${labels.join(", ")}.`, "pull request labels", 2));
|
|
275
|
+
}
|
|
276
|
+
const described = source.pullRequests.filter((pr) => pr.body.trim().length > 80).length;
|
|
277
|
+
if (described / source.pullRequests.length >= 0.7) {
|
|
278
|
+
add(facts, fact("title-body", "Most selected pull requests contain a substantive description; explain purpose, scope, and verification before requesting review.", "pull request descriptions", 3));
|
|
279
|
+
}
|
|
280
|
+
const reviewText = source.pullRequests
|
|
281
|
+
.flatMap((pr) => [...pr.comments, ...pr.reviews])
|
|
282
|
+
.join("\n")
|
|
283
|
+
.toLowerCase();
|
|
284
|
+
for (const [term, instruction] of [
|
|
285
|
+
["test", "Include or update tests when behavior changes."],
|
|
286
|
+
[
|
|
287
|
+
"changelog",
|
|
288
|
+
"Update the changelog when the repository process requires it.",
|
|
289
|
+
],
|
|
290
|
+
["documentation", "Update documentation when public behavior changes."],
|
|
291
|
+
["type", "Keep types and type-checking valid before review."],
|
|
292
|
+
]) {
|
|
293
|
+
const count = (reviewText.match(new RegExp(`\\b${term}\\w*\\b`, "g")) ?? []).length;
|
|
294
|
+
if (count >= 2) {
|
|
295
|
+
add(facts, fact("review-bar", instruction, `review discussion (${count} mentions)`, count));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (source.pullRequests.some((pr) => pr.additions + pr.deletions > 1000)) {
|
|
299
|
+
add(facts, fact("review-bar", "Keep large changes focused and explain their risk and verification in the pull request.", "pull request change sizes", 2));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (source.commits.length) {
|
|
303
|
+
const conventional = source.commits.filter((commit) => /^(feat|fix|docs|refactor|test|build|ci|chore|perf|style)(\(.+\))?!?:\s/i.test(String(commit.commit?.message ?? ""))).length;
|
|
304
|
+
if (conventional / source.commits.length >= 0.6) {
|
|
305
|
+
add(facts, fact("style", "Use conventional commit prefixes when creating commits.", "commit history", 3));
|
|
306
|
+
}
|
|
307
|
+
const examples = source.commits
|
|
308
|
+
.slice(0, 8)
|
|
309
|
+
.map((commit) => String(commit.commit?.message ?? "").split("\n")[0])
|
|
310
|
+
.filter(Boolean);
|
|
311
|
+
if (examples.length) {
|
|
312
|
+
add(facts, fact("style", `Recent commit examples: ${examples
|
|
313
|
+
.map((message) => `\`${message.slice(0, 100)}\``)
|
|
314
|
+
.join("; ")}.`, "commit history", 2));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (options.includePullRequestChanges &&
|
|
318
|
+
source.pullRequests.some((pr) => pr.changedFiles.length)) {
|
|
319
|
+
const files = counts(source.pullRequests.flatMap((pr) => pr.changedFiles)).slice(0, 10);
|
|
320
|
+
add(facts, fact("review-bar", `Frequently changed files include ${files
|
|
321
|
+
.map(([path]) => `\`${path}\``)
|
|
322
|
+
.join(", ")}; check nearby tests and workflows before editing.`, "pull request files", 2));
|
|
323
|
+
}
|
|
324
|
+
return [...facts.values()];
|
|
325
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
function reviewFacts(facts) {
|
|
2
|
+
const seen = new Set();
|
|
3
|
+
return facts
|
|
4
|
+
.filter((item) => item.sectionKey === "review-bar" &&
|
|
5
|
+
item.evidence.some((evidence) => evidence.startsWith("PR #") ||
|
|
6
|
+
evidence.startsWith("review discussion")))
|
|
7
|
+
.sort((a, b) => b.weight - a.weight || a.claim.localeCompare(b.claim))
|
|
8
|
+
.filter((item) => {
|
|
9
|
+
const key = item.claim.toLowerCase().replace(/\W+/g, " ").trim();
|
|
10
|
+
if (seen.has(key))
|
|
11
|
+
return false;
|
|
12
|
+
seen.add(key);
|
|
13
|
+
return true;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function detailedDocument(facts) {
|
|
17
|
+
const bullets = facts.flatMap((fact) => {
|
|
18
|
+
const claim = fact.claim.replace(/\bPR\s*#?\s*\d+\b/gi, "the change");
|
|
19
|
+
const lower = claim.toLowerCase();
|
|
20
|
+
const category = lower.includes("test")
|
|
21
|
+
? "Missing or insufficient tests"
|
|
22
|
+
: lower.includes("type")
|
|
23
|
+
? "Type and static validation"
|
|
24
|
+
: lower.includes("documentation")
|
|
25
|
+
? "Missing documentation"
|
|
26
|
+
: lower.includes("changelog")
|
|
27
|
+
? "Release-note completeness"
|
|
28
|
+
: lower.includes("large") || lower.includes("focused")
|
|
29
|
+
? "Unfocused or risky scope"
|
|
30
|
+
: "Repository-specific review expectation";
|
|
31
|
+
return [
|
|
32
|
+
`### ${category}`,
|
|
33
|
+
`- Violation to avoid: ${claim}`,
|
|
34
|
+
"- Inspect the affected implementation, nearby tests, and related workflow before requesting review.",
|
|
35
|
+
"- Apply this check whenever a change touches the same behavior or integration boundary.",
|
|
36
|
+
];
|
|
37
|
+
});
|
|
38
|
+
return `# Detailed PR review guide
|
|
39
|
+
|
|
40
|
+
This file is a code-review checklist derived from recurring change requests.
|
|
41
|
+
Use it while inspecting the codebase and before opening a pull request.
|
|
42
|
+
It describes failure patterns to look for, not historical pull requests.
|
|
43
|
+
|
|
44
|
+
## Code-level checks
|
|
45
|
+
|
|
46
|
+
${bullets.join("\n")}
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
export function buildReviewDocuments(facts) {
|
|
50
|
+
const selected = reviewFacts(facts);
|
|
51
|
+
if (selected.length <= 2)
|
|
52
|
+
return undefined;
|
|
53
|
+
const detailed = detailedDocument(selected.slice(0, 120));
|
|
54
|
+
const detailedLines = detailed.trimEnd().split("\n").length;
|
|
55
|
+
if (detailedLines <= 100) {
|
|
56
|
+
return {
|
|
57
|
+
guide: `# PR review guide
|
|
58
|
+
|
|
59
|
+
Use this checklist before requesting review. These expectations occurred in
|
|
60
|
+
more than two selected pull-request review signals.
|
|
61
|
+
|
|
62
|
+
## Before requesting review
|
|
63
|
+
|
|
64
|
+
${selected.map((fact) => `- ${fact.claim}`).join("\n")}
|
|
65
|
+
`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
guide: `# PR review guide
|
|
70
|
+
|
|
71
|
+
Use this file as the short checklist before requesting review. The detailed
|
|
72
|
+
code-level checks are in [PR_REVIEW_DETAILED_GUIDE.md](PR_REVIEW_DETAILED_GUIDE.md).
|
|
73
|
+
|
|
74
|
+
## Workflow
|
|
75
|
+
|
|
76
|
+
- Inspect the detailed checks that match the files and behavior being changed.
|
|
77
|
+
- Run the applicable tests and validation before requesting review.
|
|
78
|
+
- Treat the detailed checks as preventive guidance, not as a list of past PRs.
|
|
79
|
+
`,
|
|
80
|
+
detailed,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Json } from "../types.ts";
|
|
2
|
+
export type ProbeResult = {
|
|
3
|
+
includePullRequests: boolean;
|
|
4
|
+
includePullRequestChanges: boolean;
|
|
5
|
+
includeCommitHistory: boolean;
|
|
6
|
+
maxPrMonths?: number;
|
|
7
|
+
maxCommits?: number;
|
|
8
|
+
maxPullRequestChangeLines?: number;
|
|
9
|
+
report: Json;
|
|
10
|
+
reasons: string[];
|
|
11
|
+
};
|
|
12
|
+
export declare function analyzeProbe(meta: Json, pulls: Json[], detailSamples: Json[], commits: Json[]): ProbeResult;
|