co-maintainer 0.4.13 → 0.5.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 -21
- package/README.md +57 -38
- package/dist/main.js +2 -3
- package/dist/package.json +21 -15
- package/dist/src/ai/batch.d.ts +24 -1
- package/dist/src/ai/batch.js +75 -23
- package/dist/src/ai/estimate.d.ts +40 -0
- package/dist/src/ai/estimate.js +113 -0
- package/dist/src/ai/fake.d.ts +1 -1
- package/dist/src/ai/fake.js +1 -1
- package/dist/src/ai/hetzner.js +1 -1
- package/dist/src/ai/openrouter.d.ts +13 -0
- package/dist/src/ai/openrouter.js +66 -12
- package/dist/src/ai/pricing.d.ts +14 -0
- package/dist/src/ai/pricing.js +77 -0
- package/dist/src/ai/provider.js +8 -0
- package/dist/src/ai/verify.d.ts +25 -0
- package/dist/src/ai/verify.js +78 -0
- package/dist/src/cli/args.js +25 -29
- package/dist/src/cli/commands/config.d.ts +44 -0
- package/dist/src/cli/commands/config.js +220 -0
- package/dist/src/cli/commands/probe.js +70 -103
- package/dist/src/cli/commands/registry.d.ts +57 -0
- package/dist/src/cli/commands/registry.js +713 -0
- package/dist/src/cli/commands/review.js +66 -43
- package/dist/src/cli/commands/serve.d.ts +4 -2
- package/dist/src/cli/commands/serve.js +14 -10
- package/dist/src/cli/commands/set.d.ts +2 -1
- package/dist/src/cli/commands/set.js +63 -17
- package/dist/src/cli/commands/view.d.ts +1 -0
- package/dist/src/cli/commands/view.js +180 -0
- package/dist/src/cli/error.d.ts +39 -0
- package/dist/src/cli/error.js +64 -0
- package/dist/src/cli/main.d.ts +7 -0
- package/dist/src/cli/main.js +82 -1
- package/dist/src/cli/prompt.js +21 -7
- package/dist/src/cli/review_args.d.ts +4 -0
- package/dist/src/cli/review_args.js +22 -9
- package/dist/src/cli/review_output.d.ts +2 -2
- package/dist/src/cli/review_output.js +7 -6
- package/dist/src/cli/review_result.d.ts +59 -8
- package/dist/src/cli/review_result.js +145 -103
- package/dist/src/config.d.ts +15 -0
- package/dist/src/config.js +31 -0
- package/dist/src/github/app.d.ts +13 -0
- package/dist/src/github/app.js +23 -0
- package/dist/src/github/app_manifest.d.ts +50 -0
- package/dist/src/github/app_manifest.js +138 -0
- package/dist/src/github/client.js +7 -1
- package/dist/src/github/collect.js +1 -1
- package/dist/src/github/gh.js +75 -18
- package/dist/src/knowledge/facts.js +8 -3
- package/dist/src/knowledge/guide.d.ts +8 -0
- package/dist/src/knowledge/guide.js +32 -8
- package/dist/src/knowledge/probe.js +13 -3
- package/dist/src/knowledge/sections.d.ts +9 -0
- package/dist/src/knowledge/sections.js +16 -0
- package/dist/src/knowledge/skill.d.ts +5 -0
- package/dist/src/knowledge/skill.js +68 -21
- package/dist/src/knowledge/synthesis.d.ts +17 -1
- package/dist/src/knowledge/synthesis.js +70 -15
- package/dist/src/knowledge/types.d.ts +5 -0
- package/dist/src/local/codegraph_prepare.d.ts +3 -2
- package/dist/src/local/codegraph_prepare.js +4 -2
- package/dist/src/local/git_ops.d.ts +5 -4
- package/dist/src/local/git_ops.js +8 -9
- package/dist/src/local/review_local.js +36 -14
- package/dist/src/pr/checkout.js +14 -3
- package/dist/src/pr/codegraph_tools.js +3 -3
- package/dist/src/pr/diff_summary.js +3 -3
- package/dist/src/pr/findings.js +16 -5
- package/dist/src/pr/findings_json.d.ts +26 -0
- package/dist/src/pr/findings_json.js +187 -0
- package/dist/src/pr/review_copy.d.ts +20 -0
- package/dist/src/pr/review_copy.js +40 -0
- package/dist/src/pr/reviewer.d.ts +2 -1
- package/dist/src/pr/reviewer.js +84 -91
- package/dist/src/remote/client.js +42 -42
- package/dist/src/remote/http.d.ts +5 -0
- package/dist/src/remote/http.js +49 -0
- package/dist/src/remote/server/guides.d.ts +22 -0
- package/dist/src/remote/server/guides.js +81 -0
- package/dist/src/remote/server/routes.js +24 -0
- package/dist/src/review/blocking.d.ts +35 -0
- package/dist/src/review/blocking.js +45 -0
- package/dist/src/review/carry_over.d.ts +12 -1
- package/dist/src/review/carry_over.js +35 -9
- package/dist/src/review/engine.d.ts +1 -0
- package/dist/src/review/guides.js +28 -1
- package/dist/src/server/api/installations.js +1 -1
- package/dist/src/server/api/repos.js +86 -3
- package/dist/src/server/api/settings.js +3 -3
- package/dist/src/server/app.d.ts +3 -0
- package/dist/src/server/app.js +12 -5
- package/dist/src/server/pages/activity.d.ts +1 -0
- package/dist/src/server/pages/activity.js +2 -2
- package/dist/src/server/pages/add_repo.js +73 -5
- package/dist/src/server/pages/client.d.ts +1 -1
- package/dist/src/server/pages/client.js +1 -1
- package/dist/src/server/pages/home.d.ts +6 -1
- package/dist/src/server/pages/home.js +25 -2
- package/dist/src/server/pages/knowledge.js +1 -1
- package/dist/src/server/pages/layout.js +2 -2
- package/dist/src/server/pages/repo.d.ts +1 -1
- package/dist/src/server/pages/repo.js +21 -2
- package/dist/src/server/pages/repo_prs.d.ts +5 -1
- package/dist/src/server/pages/repo_prs.js +36 -1
- package/dist/src/server/pages/repo_remote.js +2 -2
- package/dist/src/server/pages/repo_settings.js +2 -2
- package/dist/src/server/pages/router.d.ts +6 -0
- package/dist/src/server/pages/router.js +129 -9
- package/dist/src/server/pages/settings.d.ts +1 -1
- package/dist/src/server/pages/settings.js +87 -9
- package/dist/src/server/pages/styles.d.ts +1 -1
- package/dist/src/server/pages/styles.js +1 -1
- package/dist/src/services/probe.d.ts +28 -0
- package/dist/src/services/probe.js +133 -0
- package/dist/src/services/remake_cron.d.ts +1 -1
- package/dist/src/services/remake_cron.js +3 -3
- package/dist/src/services/remote_review.js +14 -4
- package/dist/src/services/review.d.ts +11 -0
- package/dist/src/services/review.js +39 -11
- package/dist/src/services/setup.js +37 -19
- package/dist/src/services/setup_checklist.d.ts +10 -0
- package/dist/src/services/setup_checklist.js +87 -0
- package/dist/src/store/app_db.js +5 -2
- package/dist/src/store/cache_db.d.ts +4 -0
- package/dist/src/store/cache_db.js +27 -4
- package/dist/src/store/deliveries.d.ts +7 -0
- package/dist/src/store/deliveries.js +15 -0
- package/dist/src/tools/codegraph.d.ts +30 -0
- package/dist/src/tools/codegraph.js +49 -12
- package/dist/src/types.d.ts +10 -0
- package/dist/src/util/log.d.ts +3 -0
- package/dist/src/util/log.js +12 -0
- package/dist/src/util/run_summary.d.ts +33 -0
- package/dist/src/util/run_summary.js +47 -0
- package/dist/src/util/webhook_reachability.d.ts +11 -0
- package/dist/src/util/webhook_reachability.js +64 -0
- package/package.json +21 -15
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* (pat.ts, and app.ts). `GhClient` shells out to the `gh` binary and does
|
|
3
3
|
* not use `githubFetch`; both paths share `paginate` and `waitForRateLimit`. */
|
|
4
4
|
import { log } from "../util/log.js";
|
|
5
|
+
import { networkFailure } from "../cli/error.js";
|
|
5
6
|
export class GitHubHttpError extends Error {
|
|
6
7
|
status;
|
|
7
8
|
constructor(status, body) {
|
|
@@ -85,7 +86,12 @@ export async function githubFetch(url, init) {
|
|
|
85
86
|
let response;
|
|
86
87
|
let exhausted;
|
|
87
88
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
88
|
-
|
|
89
|
+
try {
|
|
90
|
+
response = await fetch(url, init);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
throw networkFailure(url, error);
|
|
94
|
+
}
|
|
89
95
|
if (response.status !== 403 && response.status !== 429)
|
|
90
96
|
return response;
|
|
91
97
|
const retryAfterHeader = response.headers.get("retry-after");
|
|
@@ -286,7 +286,7 @@ async function pullRequests(client, options, previous, phase, progress) {
|
|
|
286
286
|
detail = await client.request(`repos/${options.repo}/pulls/${number}`);
|
|
287
287
|
}
|
|
288
288
|
catch (error) {
|
|
289
|
-
log("fetch", `PR #${number} detail unavailable
|
|
289
|
+
log("fetch", `PR #${number} detail unavailable. Diff skipped: ${String(error)}`);
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
292
|
const current = {
|
package/dist/src/github/gh.js
CHANGED
|
@@ -1,7 +1,44 @@
|
|
|
1
1
|
import { paginate, probeDelay, RATE_LIMIT_PROBE_MS, rateLimitError, waitForRateLimit, } from "./client.js";
|
|
2
2
|
import { log } from "../util/log.js";
|
|
3
|
-
import {
|
|
3
|
+
import { CliError, EXIT_RUNTIME, EXIT_USAGE } from "../cli/error.js";
|
|
4
|
+
import { commandOutput, commandWithInput, getEnv, } from "../util/runtime.js";
|
|
4
5
|
const PULSE_MS = 120_000;
|
|
6
|
+
/** Test seam: `CM_GH_BIN` replaces the `gh` command and `CM_GH_SCRIPT` (if
|
|
7
|
+
* set) is prepended as its first argument, so a fake can be run as
|
|
8
|
+
* `<bin> <script> api …` with no shell. Unset means the real `gh` on PATH, so
|
|
9
|
+
* production behaviour is unchanged. Spawning without a shell matters: a shell
|
|
10
|
+
* concatenates arguments unescaped, so an endpoint's `?`, `&` and `=` would be
|
|
11
|
+
* reinterpreted by cmd.exe on Windows. */
|
|
12
|
+
function ghSpawn() {
|
|
13
|
+
const bin = getEnv("CM_GH_BIN");
|
|
14
|
+
if (!bin)
|
|
15
|
+
return { command: "gh", prefix: [] };
|
|
16
|
+
const script = getEnv("CM_GH_SCRIPT");
|
|
17
|
+
return { command: bin, prefix: script ? [script] : [] };
|
|
18
|
+
}
|
|
19
|
+
/** Turns a failed `gh api` call into a message that says what broke, why, and
|
|
20
|
+
* what to do next (CORE-12, F09/F24/F28). The four shapes the DX research hit:
|
|
21
|
+
* `gh` missing from PATH, the repo missing or unreadable, an empty stderr, and
|
|
22
|
+
* everything else. The first line is the only part a human reads, so it always
|
|
23
|
+
* names the situation; the raw gh text moves to the hint so it stays available
|
|
24
|
+
* without drowning the message. */
|
|
25
|
+
function ghFailure(endpoint, stderr) {
|
|
26
|
+
if (/ENOENT|command not found|not recognized/i.test(stderr)) {
|
|
27
|
+
return new CliError("gh_not_installed", "GitHub CLI (gh) was not found.", "Install it from https://cli.github.com or use --auth=pat.", EXIT_USAGE);
|
|
28
|
+
}
|
|
29
|
+
if (/HTTP 404|Not Found/i.test(stderr)) {
|
|
30
|
+
// The endpoint is `repos/owner/repo/...`; the user thinks in `owner/repo`.
|
|
31
|
+
const match = endpoint.match(/^repos\/([^/]+)\/([^/]+)/);
|
|
32
|
+
const subject = match ? `${match[1]}/${match[2]}` : endpoint;
|
|
33
|
+
return new CliError("repo_not_found", `${subject} was not found, or your GitHub account cannot read it.`, "Check the name and run gh auth status.", EXIT_USAGE);
|
|
34
|
+
}
|
|
35
|
+
// An empty stderr used to collapse to just the endpoint, which told the user
|
|
36
|
+
// nothing about what happened.
|
|
37
|
+
if (stderr.length === 0) {
|
|
38
|
+
return new CliError("gh_failed", `gh exited without an error message while reading ${endpoint}.`, "Run gh auth status and try again with --debug.", EXIT_RUNTIME);
|
|
39
|
+
}
|
|
40
|
+
return new CliError("gh_failed", `gh could not read ${endpoint}.`, `gh said: ${stderr}`, EXIT_RUNTIME);
|
|
41
|
+
}
|
|
5
42
|
let lastCallAt = 0;
|
|
6
43
|
let pulse;
|
|
7
44
|
function count(value) {
|
|
@@ -23,8 +60,9 @@ export function quotaLine(body) {
|
|
|
23
60
|
}
|
|
24
61
|
async function printQuota() {
|
|
25
62
|
try {
|
|
26
|
-
const
|
|
27
|
-
|
|
63
|
+
const { command, prefix } = ghSpawn();
|
|
64
|
+
const result = await commandOutput(command, {
|
|
65
|
+
args: [...prefix, "api", "rate_limit"],
|
|
28
66
|
stdout: "piped",
|
|
29
67
|
stderr: "piped",
|
|
30
68
|
});
|
|
@@ -62,8 +100,9 @@ function noteGhCall(debug) {
|
|
|
62
100
|
}
|
|
63
101
|
async function readLimit(endpoint) {
|
|
64
102
|
try {
|
|
65
|
-
const
|
|
66
|
-
|
|
103
|
+
const { command, prefix } = ghSpawn();
|
|
104
|
+
const result = await commandOutput(command, {
|
|
105
|
+
args: [...prefix, "api", "rate_limit"],
|
|
67
106
|
stdout: "piped",
|
|
68
107
|
stderr: "piped",
|
|
69
108
|
});
|
|
@@ -90,11 +129,14 @@ export class GhClient {
|
|
|
90
129
|
this.debug = debug;
|
|
91
130
|
}
|
|
92
131
|
request(endpoint) {
|
|
93
|
-
return this.call(endpoint, () =>
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
132
|
+
return this.call(endpoint, () => {
|
|
133
|
+
const { command, prefix } = ghSpawn();
|
|
134
|
+
return commandOutput(command, {
|
|
135
|
+
args: [...prefix, "api", endpoint],
|
|
136
|
+
stdout: "piped",
|
|
137
|
+
stderr: "piped",
|
|
138
|
+
});
|
|
139
|
+
});
|
|
98
140
|
}
|
|
99
141
|
write(endpoint, body) {
|
|
100
142
|
return this.send(endpoint, "POST", body);
|
|
@@ -109,19 +151,34 @@ export class GhClient {
|
|
|
109
151
|
return paginate((e) => this.request(e), endpoint, limit, progress);
|
|
110
152
|
}
|
|
111
153
|
send(endpoint, method, body) {
|
|
112
|
-
return this.call(endpoint, () =>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
154
|
+
return this.call(endpoint, () => {
|
|
155
|
+
const { command, prefix } = ghSpawn();
|
|
156
|
+
return commandWithInput(command, {
|
|
157
|
+
args: [...prefix, "api", "-X", method, endpoint, "--input", "-"],
|
|
158
|
+
stdin: "piped",
|
|
159
|
+
stdout: "piped",
|
|
160
|
+
stderr: "piped",
|
|
161
|
+
}, JSON.stringify(body));
|
|
162
|
+
});
|
|
118
163
|
}
|
|
119
164
|
async call(endpoint, run) {
|
|
120
165
|
noteGhCall(this.debug);
|
|
121
166
|
const started = Date.now();
|
|
122
167
|
let spun = false;
|
|
123
168
|
for (;;) {
|
|
124
|
-
|
|
169
|
+
// A missing `gh` binary rejects the spawn instead of returning a failed
|
|
170
|
+
// result, so the ENOENT case has to be caught here too (CORE-12).
|
|
171
|
+
let result;
|
|
172
|
+
try {
|
|
173
|
+
result = await run();
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
177
|
+
if (/ENOENT/i.test(message)) {
|
|
178
|
+
throw new CliError("gh_not_installed", "GitHub CLI (gh) was not found.", "Install it from https://cli.github.com or use --auth=pat.", EXIT_USAGE);
|
|
179
|
+
}
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
125
182
|
if (result.success) {
|
|
126
183
|
try {
|
|
127
184
|
return JSON.parse(new TextDecoder().decode(result.stdout));
|
|
@@ -136,7 +193,7 @@ export class GhClient {
|
|
|
136
193
|
? bucket.remaining === 0
|
|
137
194
|
: /rate limit/i.test(error);
|
|
138
195
|
if (!limited)
|
|
139
|
-
throw
|
|
196
|
+
throw ghFailure(endpoint, error);
|
|
140
197
|
const resetAt = bucket?.resetAt ?? new Date(Date.now() + RATE_LIMIT_PROBE_MS);
|
|
141
198
|
const delay = probeDelay(resetAt, Date.now());
|
|
142
199
|
if (delay === 0) {
|
|
@@ -266,7 +266,7 @@ export function extractFacts(source, options) {
|
|
|
266
266
|
}
|
|
267
267
|
if (source.pullRequests.length) {
|
|
268
268
|
const merged = source.pullRequests.filter((pr) => pr.merged).length;
|
|
269
|
-
add(facts, fact("process", `${merged}/${source.pullRequests.length} selected pull requests are merged
|
|
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
270
|
const labels = counts(source.pullRequests.flatMap((pr) => pr.labels))
|
|
271
271
|
.slice(0, 12)
|
|
272
272
|
.map(([label, count]) => `\`${label}\` (${count})`);
|
|
@@ -275,7 +275,7 @@ export function extractFacts(source, options) {
|
|
|
275
275
|
}
|
|
276
276
|
const described = source.pullRequests.filter((pr) => pr.body.trim().length > 80).length;
|
|
277
277
|
if (described / source.pullRequests.length >= 0.7) {
|
|
278
|
-
add(facts, fact("title-body", "Most selected pull requests contain a substantive description
|
|
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
279
|
}
|
|
280
280
|
const reviewText = source.pullRequests
|
|
281
281
|
.flatMap((pr) => [...pr.comments, ...pr.reviews])
|
|
@@ -292,7 +292,12 @@ export function extractFacts(source, options) {
|
|
|
292
292
|
]) {
|
|
293
293
|
const count = (reviewText.match(new RegExp(`\\b${term}\\w*\\b`, "g")) ?? []).length;
|
|
294
294
|
if (count >= 2) {
|
|
295
|
-
add(facts,
|
|
295
|
+
add(facts, {
|
|
296
|
+
...fact("review-bar", instruction, `review discussion (${count} mentions)`, count),
|
|
297
|
+
// A rule mined from discussion across several pull requests is
|
|
298
|
+
// repository policy, not one request's narrative (CORE-32).
|
|
299
|
+
origin: "repository",
|
|
300
|
+
});
|
|
296
301
|
}
|
|
297
302
|
}
|
|
298
303
|
if (source.pullRequests.some((pr) => pr.additions + pr.deletions > 1000)) {
|
|
@@ -3,4 +3,12 @@ export type ReviewDocuments = {
|
|
|
3
3
|
guide: string;
|
|
4
4
|
detailed?: string;
|
|
5
5
|
};
|
|
6
|
+
/** A fact read out of a single pull request describes that request, not the
|
|
7
|
+
* repository. `origin` is absent on facts written before the field existed, so
|
|
8
|
+
* those count as repository policy (CORE-32 / F26b). */
|
|
9
|
+
export declare function isPullRequestFact(item: Fact): boolean;
|
|
10
|
+
/** The selected review signals, before the `>2` threshold is applied. Exported
|
|
11
|
+
* so `init` can tell the user how far short of the threshold a repository is
|
|
12
|
+
* (CORE-31), rather than silently skipping the two guide files. */
|
|
13
|
+
export declare function reviewSignalCount(facts: Fact[]): number;
|
|
6
14
|
export declare function buildReviewDocuments(facts: Fact[]): ReviewDocuments | undefined;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
/** A fact read out of a single pull request describes that request, not the
|
|
2
|
+
* repository. `origin` is absent on facts written before the field existed, so
|
|
3
|
+
* those count as repository policy (CORE-32 / F26b). */
|
|
4
|
+
export function isPullRequestFact(item) {
|
|
5
|
+
return item.origin === "pull-request";
|
|
6
|
+
}
|
|
7
|
+
function dedupe(items) {
|
|
2
8
|
const seen = new Set();
|
|
3
|
-
return
|
|
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
|
+
return items.filter((item) => {
|
|
9
10
|
const key = item.claim.toLowerCase().replace(/\W+/g, " ").trim();
|
|
10
11
|
if (seen.has(key))
|
|
11
12
|
return false;
|
|
@@ -13,6 +14,23 @@ function reviewFacts(facts) {
|
|
|
13
14
|
return true;
|
|
14
15
|
});
|
|
15
16
|
}
|
|
17
|
+
function reviewBarFacts(facts) {
|
|
18
|
+
return facts
|
|
19
|
+
.filter((item) => item.sectionKey === "review-bar" &&
|
|
20
|
+
item.evidence.some((evidence) => evidence.startsWith("PR #") ||
|
|
21
|
+
evidence.startsWith("review discussion")))
|
|
22
|
+
.sort((a, b) => b.weight - a.weight || a.claim.localeCompare(b.claim));
|
|
23
|
+
}
|
|
24
|
+
/** The short guide's checklist: every recurring review signal, including the
|
|
25
|
+
* ones mined from pull requests (CORE-32 keeps this file's PR source). */
|
|
26
|
+
function reviewFacts(facts) {
|
|
27
|
+
return dedupe(reviewBarFacts(facts));
|
|
28
|
+
}
|
|
29
|
+
/** The detailed guide's checklist: only expectations backed by repository-wide
|
|
30
|
+
* evidence, never a single pull request's narrative (CORE-32). */
|
|
31
|
+
function detailedFacts(facts) {
|
|
32
|
+
return dedupe(reviewBarFacts(facts).filter((item) => !isPullRequestFact(item)));
|
|
33
|
+
}
|
|
16
34
|
function detailedDocument(facts) {
|
|
17
35
|
const bullets = facts.flatMap((fact) => {
|
|
18
36
|
const claim = fact.claim.replace(/\bPR\s*#?\s*\d+\b/gi, "the change");
|
|
@@ -46,11 +64,17 @@ It describes failure patterns to look for, not historical pull requests.
|
|
|
46
64
|
${bullets.join("\n")}
|
|
47
65
|
`;
|
|
48
66
|
}
|
|
67
|
+
/** The selected review signals, before the `>2` threshold is applied. Exported
|
|
68
|
+
* so `init` can tell the user how far short of the threshold a repository is
|
|
69
|
+
* (CORE-31), rather than silently skipping the two guide files. */
|
|
70
|
+
export function reviewSignalCount(facts) {
|
|
71
|
+
return reviewFacts(facts).length;
|
|
72
|
+
}
|
|
49
73
|
export function buildReviewDocuments(facts) {
|
|
50
74
|
const selected = reviewFacts(facts);
|
|
51
75
|
if (selected.length <= 2)
|
|
52
76
|
return undefined;
|
|
53
|
-
const detailed = detailedDocument(
|
|
77
|
+
const detailed = detailedDocument(detailedFacts(facts).slice(0, 120));
|
|
54
78
|
const detailedLines = detailed.trimEnd().split("\n").length;
|
|
55
79
|
if (detailedLines <= 100) {
|
|
56
80
|
return {
|
|
@@ -103,7 +103,7 @@ export function analyzeProbe(meta, pulls, detailSamples, commits) {
|
|
|
103
103
|
: Infinity;
|
|
104
104
|
reasons.push(releaseAgeMonths <= 18
|
|
105
105
|
? `A release from ${latestReleaseAt.slice(0, 10)} is recent; use the last ${maxPrMonths} months to prioritize the current contribution and release process.`
|
|
106
|
-
: `The last ${maxPrMonths} months cover at least 85% of pull requests
|
|
106
|
+
: `The last ${maxPrMonths} months cover at least 85% of pull requests. Older history is a minority.`);
|
|
107
107
|
}
|
|
108
108
|
else if (pulls.length) {
|
|
109
109
|
reasons.push("Pull request volume or history span is small enough to keep all PR months.");
|
|
@@ -112,10 +112,20 @@ export function analyzeProbe(meta, pulls, detailSamples, commits) {
|
|
|
112
112
|
reasons.push(`${usefulCommits} non-merge commits contain enough signal to analyze commit conventions.`);
|
|
113
113
|
}
|
|
114
114
|
else if (commits.length) {
|
|
115
|
-
|
|
115
|
+
// Only claim the history is noise when the useful-commit ratio actually
|
|
116
|
+
// supports it. With `commits: 8 · useful: 5` the ratio is above a half,
|
|
117
|
+
// and the old unconditional sentence contradicted the numbers beside it
|
|
118
|
+
// (CORE-24).
|
|
119
|
+
const usefulRatio = usefulCommits / commits.length;
|
|
120
|
+
if (usefulRatio < 0.5) {
|
|
121
|
+
reasons.push("Commit history is mostly merge/noise commits, so it is not recommended as a source.");
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
reasons.push(`Commit history is short (${commits.length} ${commits.length === 1 ? "commit" : "commits"}), so it is not recommended as a source yet.`);
|
|
125
|
+
}
|
|
116
126
|
}
|
|
117
127
|
if (maxChangeLines) {
|
|
118
|
-
reasons.push(`The sampled diff p90 is ${p90Diff} lines
|
|
128
|
+
reasons.push(`The sampled diff p90 is ${p90Diff} lines. The cap keeps the largest outliers out while retaining about 90% of sampled diffs.`);
|
|
119
129
|
}
|
|
120
130
|
return {
|
|
121
131
|
includePullRequests: pulls.length > 0,
|
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
export declare const sectionTitles: Record<string, string>;
|
|
2
2
|
export declare const sectionKeys: string[];
|
|
3
|
+
/** Sections whose content also lives in `CODEBASE.md`. The skill links to that
|
|
4
|
+
* file instead of keeping a second copy (CORE-32 / F26c). */
|
|
5
|
+
export declare const codebaseSectionKeys: Set<string>;
|
|
6
|
+
/** A fact read out of a single pull request describes that request, not the
|
|
7
|
+
* repository, so it may only feed the `review-bar` checklist. Every other
|
|
8
|
+
* section states repository policy and must not learn from one PR's narrative
|
|
9
|
+
* (CORE-32 / F26b). Keep this in sync with `guide.ts`, which applies the same
|
|
10
|
+
* rule to the review guides. */
|
|
11
|
+
export declare function sectionAllowsPrFacts(key: string): boolean;
|
|
@@ -11,3 +11,19 @@ export const sectionTitles = {
|
|
|
11
11
|
process: "Pull request process",
|
|
12
12
|
};
|
|
13
13
|
export const sectionKeys = Object.keys(sectionTitles);
|
|
14
|
+
/** Sections whose content also lives in `CODEBASE.md`. The skill links to that
|
|
15
|
+
* file instead of keeping a second copy (CORE-32 / F26c). */
|
|
16
|
+
export const codebaseSectionKeys = new Set([
|
|
17
|
+
"layout",
|
|
18
|
+
"style",
|
|
19
|
+
"tests",
|
|
20
|
+
"devloop",
|
|
21
|
+
]);
|
|
22
|
+
/** A fact read out of a single pull request describes that request, not the
|
|
23
|
+
* repository, so it may only feed the `review-bar` checklist. Every other
|
|
24
|
+
* section states repository policy and must not learn from one PR's narrative
|
|
25
|
+
* (CORE-32 / F26b). Keep this in sync with `guide.ts`, which applies the same
|
|
26
|
+
* rule to the review guides. */
|
|
27
|
+
export function sectionAllowsPrFacts(key) {
|
|
28
|
+
return key === "review-bar";
|
|
29
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { Fact } from "./types.ts";
|
|
2
|
+
/** The full text of the sections that live in `CODEBASE.md`. The skill itself
|
|
3
|
+
* only links to that file, so this is the single place the codebase guidance is
|
|
4
|
+
* written out: the synthesized override when there is one, otherwise the
|
|
5
|
+
* rendered facts (CORE-32 / F26c). */
|
|
6
|
+
export declare function codebaseBody(facts: Fact[], overrides?: Record<string, string>): string;
|
|
2
7
|
/** Slices an assembled skill's `## Heading` sections back out by section key,
|
|
3
8
|
* from `<!-- section:key -->` markers or (fallback) canonical heading text. */
|
|
4
9
|
export declare function extractSections(markdown: string): Record<string, string>;
|
|
@@ -1,15 +1,54 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isPullRequestFact } from "./guide.js";
|
|
2
|
+
import { codebaseSectionKeys, sectionAllowsPrFacts, sectionKeys, sectionTitles, } from "./sections.js";
|
|
2
3
|
function sectionFacts(facts, key) {
|
|
3
4
|
return facts
|
|
4
5
|
.filter((item) => item.sectionKey === key)
|
|
5
6
|
.sort((a, b) => b.weight - a.weight);
|
|
6
7
|
}
|
|
8
|
+
/** Facts a section may state. A fact read out of a single pull request is that
|
|
9
|
+
* request's narrative, not a rule, so only `review-bar` may use it (CORE-32 /
|
|
10
|
+
* F26b). Facts written before `origin` existed count as repository policy. */
|
|
11
|
+
function usableFacts(facts, key) {
|
|
12
|
+
const items = sectionFacts(facts, key);
|
|
13
|
+
return sectionAllowsPrFacts(key)
|
|
14
|
+
? items
|
|
15
|
+
: items.filter((item) => !isPullRequestFact(item));
|
|
16
|
+
}
|
|
17
|
+
/** The link that stands in for a codebase section's text. */
|
|
18
|
+
function isCodebaseLink(section) {
|
|
19
|
+
return section.includes("[CODEBASE.md](CODEBASE.md)");
|
|
20
|
+
}
|
|
7
21
|
function render(key, facts) {
|
|
8
|
-
|
|
22
|
+
if (codebaseSectionKeys.has(key)) {
|
|
23
|
+
return `## ${sectionTitles[key] ?? key}\n\nSee [CODEBASE.md](CODEBASE.md).`;
|
|
24
|
+
}
|
|
25
|
+
const items = usableFacts(facts, key).slice(0, 6);
|
|
9
26
|
if (!items.length)
|
|
10
27
|
return "";
|
|
11
28
|
return `## ${sectionTitles[key] ?? key}\n\n${items.map((item) => `- ${item.claim}`).join("\n")}`;
|
|
12
29
|
}
|
|
30
|
+
function codebaseSectionText(key, facts, overrides) {
|
|
31
|
+
const override = overrides[key]?.trim();
|
|
32
|
+
if (override && hasBullets(override) && !isCodebaseLink(override)) {
|
|
33
|
+
return cleanSection(override);
|
|
34
|
+
}
|
|
35
|
+
const items = usableFacts(facts, key).slice(0, 6);
|
|
36
|
+
if (!items.length)
|
|
37
|
+
return "";
|
|
38
|
+
return `## ${sectionTitles[key] ?? key}\n\n${items
|
|
39
|
+
.map((item) => `- ${item.claim}`)
|
|
40
|
+
.join("\n")}`;
|
|
41
|
+
}
|
|
42
|
+
/** The full text of the sections that live in `CODEBASE.md`. The skill itself
|
|
43
|
+
* only links to that file, so this is the single place the codebase guidance is
|
|
44
|
+
* written out: the synthesized override when there is one, otherwise the
|
|
45
|
+
* rendered facts (CORE-32 / F26c). */
|
|
46
|
+
export function codebaseBody(facts, overrides = {}) {
|
|
47
|
+
return [...codebaseSectionKeys]
|
|
48
|
+
.map((key) => codebaseSectionText(key, facts, overrides))
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.join("\n\n");
|
|
51
|
+
}
|
|
13
52
|
function cleanSection(value) {
|
|
14
53
|
return value.replace(/^\s*```[a-zA-Z0-9_-]*\s*$/gm, "").trim();
|
|
15
54
|
}
|
|
@@ -42,18 +81,25 @@ async function hash(value) {
|
|
|
42
81
|
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
43
82
|
.join("");
|
|
44
83
|
}
|
|
84
|
+
/** The fields that decide a section's text. `origin` is included so a fact
|
|
85
|
+
* moving from PR narrative to repository policy (or the reverse) counts as a
|
|
86
|
+
* change rather than reusing a section built under the old rule (CORE-32). */
|
|
87
|
+
function hashInput(item) {
|
|
88
|
+
return [
|
|
89
|
+
item.id,
|
|
90
|
+
item.claim,
|
|
91
|
+
item.weight,
|
|
92
|
+
item.scope,
|
|
93
|
+
item.confidence,
|
|
94
|
+
item.status,
|
|
95
|
+
item.origin ?? "repository",
|
|
96
|
+
];
|
|
97
|
+
}
|
|
45
98
|
export async function factSectionHashes(facts) {
|
|
46
99
|
const hashes = {};
|
|
47
100
|
for (const key of new Set(facts.map((item) => item.sectionKey))) {
|
|
48
101
|
const relevant = sectionFacts(facts, key);
|
|
49
|
-
hashes[key] = await hash(JSON.stringify(relevant.map(
|
|
50
|
-
item.id,
|
|
51
|
-
item.claim,
|
|
52
|
-
item.weight,
|
|
53
|
-
item.scope,
|
|
54
|
-
item.confidence,
|
|
55
|
-
item.status,
|
|
56
|
-
])));
|
|
102
|
+
hashes[key] = await hash(JSON.stringify(relevant.map(hashInput)));
|
|
57
103
|
}
|
|
58
104
|
return hashes;
|
|
59
105
|
}
|
|
@@ -68,26 +114,27 @@ export async function assembleSkill(repo, facts, previousMarkdown, previousHashe
|
|
|
68
114
|
if (!content)
|
|
69
115
|
continue;
|
|
70
116
|
const relevant = sectionFacts(facts, key);
|
|
71
|
-
const sectionHash = await hash(JSON.stringify(relevant.map(
|
|
72
|
-
item.id,
|
|
73
|
-
item.claim,
|
|
74
|
-
item.weight,
|
|
75
|
-
item.scope,
|
|
76
|
-
item.confidence,
|
|
77
|
-
item.status,
|
|
78
|
-
])));
|
|
117
|
+
const sectionHash = await hash(JSON.stringify(relevant.map(hashInput)));
|
|
79
118
|
hashes[key] = sectionHash;
|
|
80
119
|
const canonicalHeading = `## ${sectionTitles[key] ?? key}`;
|
|
81
|
-
|
|
120
|
+
// The codebase sections hold a link, not text, so the skill never takes a
|
|
121
|
+
// synthesized override for them; the override goes to CODEBASE.md instead.
|
|
122
|
+
const hasOverride = Object.hasOwn(overrides, key) && !codebaseSectionKeys.has(key);
|
|
82
123
|
if (!hasOverride &&
|
|
83
124
|
previousMarkdown &&
|
|
84
125
|
previousHashes[key] === sectionHash &&
|
|
85
|
-
previous[key]?.startsWith(canonicalHeading)
|
|
126
|
+
previous[key]?.startsWith(canonicalHeading) &&
|
|
127
|
+
// A section whose shape changed (the codebase sections now carry a link
|
|
128
|
+
// instead of the text) is rewritten rather than kept because its facts
|
|
129
|
+
// happen to be unchanged.
|
|
130
|
+
isCodebaseLink(content) === isCodebaseLink(previous[key])) {
|
|
86
131
|
sections[key] = cleanSection(previous[key]);
|
|
87
132
|
continue;
|
|
88
133
|
}
|
|
89
134
|
const section = hasOverride ? overrides[key] : content;
|
|
90
|
-
|
|
135
|
+
// A codebase section is a link, not a bullet list, so it is kept even
|
|
136
|
+
// though it has no bullets; a text section without bullets is dropped.
|
|
137
|
+
if (!isCodebaseLink(section) && !hasBullets(section))
|
|
91
138
|
continue;
|
|
92
139
|
sections[key] = section;
|
|
93
140
|
changed.push(key);
|
|
@@ -1,7 +1,23 @@
|
|
|
1
|
+
import type { SkippedUnit } from "../ai/batch.ts";
|
|
1
2
|
import type { AiProvider, AiResponse, Options } from "../types.ts";
|
|
2
3
|
import type { Fact, Source } from "./types.ts";
|
|
3
4
|
type UsageSink = (job: string, response: AiResponse) => Promise<void>;
|
|
5
|
+
export type SkippedUnits = SkippedUnit[];
|
|
6
|
+
export type SynthesisResult = Record<string, string>;
|
|
4
7
|
export declare function extractAiFacts(provider: AiProvider, repo: string, source: Source, options: Options, usage?: UsageSink): Promise<Fact[]>;
|
|
8
|
+
export declare function extractAiFactsWithReport(provider: AiProvider, repo: string, source: Source, options: Options, usage?: UsageSink): Promise<{
|
|
9
|
+
facts: Fact[];
|
|
10
|
+
skipped: SkippedUnit[];
|
|
11
|
+
}>;
|
|
12
|
+
/** Same as {@link extractAiFacts}, but also reports what the batch skipped so
|
|
13
|
+
* the caller can put it in the final `[done]` line (CORE-30). */
|
|
14
|
+
export declare function enrichFactsWithReport(provider: AiProvider, repo: string, base: Fact[], source: Source, options: Options, usage?: UsageSink): Promise<{
|
|
15
|
+
facts: Fact[];
|
|
16
|
+
skipped: SkippedUnit[];
|
|
17
|
+
}>;
|
|
5
18
|
export declare function enrichFacts(provider: AiProvider, repo: string, base: Fact[], source: Source, options: Options, usage?: UsageSink): Promise<Fact[]>;
|
|
6
|
-
export declare function synthesizeSections(provider: AiProvider, repo: string, facts: Fact[], _previousMarkdown: string | undefined, ai: Options["ai"], model: string, concurrency: number, usage?: UsageSink, onlySections?: Set<string>): Promise<
|
|
19
|
+
export declare function synthesizeSections(provider: AiProvider, repo: string, facts: Fact[], _previousMarkdown: string | undefined, ai: Options["ai"], model: string, concurrency: number, usage?: UsageSink, onlySections?: Set<string>): Promise<{
|
|
20
|
+
overrides: Record<string, string>;
|
|
21
|
+
skipped: SkippedUnit[];
|
|
22
|
+
}>;
|
|
7
23
|
export {};
|