@oss-autopilot/core 3.4.1 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-registry.js +50 -0
- package/dist/cli.bundle.cjs +81 -78
- package/dist/commands/compliance-score.d.ts +21 -0
- package/dist/commands/compliance-score.js +156 -0
- package/dist/commands/index.d.ts +4 -0
- package/dist/commands/index.js +4 -0
- package/dist/commands/list-mark-done.d.ts +48 -0
- package/dist/commands/list-mark-done.js +213 -0
- package/dist/commands/parse-list.js +86 -9
- package/dist/commands/repo-vet.d.ts +21 -0
- package/dist/commands/repo-vet.js +215 -0
- package/dist/commands/startup.js +18 -0
- package/dist/core/ci-enforced-tools.d.ts +35 -0
- package/dist/core/ci-enforced-tools.js +109 -0
- package/dist/core/comment-decision.d.ts +72 -0
- package/dist/core/comment-decision.js +74 -0
- package/dist/core/compliance-score.d.ts +127 -0
- package/dist/core/compliance-score.js +277 -0
- package/dist/core/config-registry.js +12 -0
- package/dist/core/contributing.d.ts +52 -0
- package/dist/core/contributing.js +139 -0
- package/dist/core/extraction-categories.d.ts +55 -0
- package/dist/core/extraction-categories.js +108 -0
- package/dist/core/follow-up-history.d.ts +41 -0
- package/dist/core/follow-up-history.js +71 -0
- package/dist/core/gist-state-store.d.ts +30 -7
- package/dist/core/gist-state-store.js +87 -11
- package/dist/core/issue-conversation.js +1 -0
- package/dist/core/issue-effort.d.ts +29 -0
- package/dist/core/issue-effort.js +41 -0
- package/dist/core/maintainer-hints.d.ts +23 -0
- package/dist/core/maintainer-hints.js +36 -0
- package/dist/core/pr-quality-rubric.d.ts +70 -0
- package/dist/core/pr-quality-rubric.js +121 -0
- package/dist/core/repo-vet.d.ts +90 -0
- package/dist/core/repo-vet.js +178 -0
- package/dist/core/state-schema.d.ts +76 -0
- package/dist/core/state-schema.js +75 -0
- package/dist/core/strategy.d.ts +75 -0
- package/dist/core/strategy.js +226 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/core/workflow-state.d.ts +56 -0
- package/dist/core/workflow-state.js +101 -0
- package/dist/formatters/json.d.ts +147 -0
- package/dist/formatters/json.js +79 -0
- package/package.json +1 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* repo-vet command (#1271, follow-up to #1242).
|
|
3
|
+
*
|
|
4
|
+
* Fetches repo health signals via the GitHub API, runs the typed
|
|
5
|
+
* `computeRepoVet` core function, and returns the structured result.
|
|
6
|
+
* Used by the `repo-evaluator` agent (and the future `repo-vet` MCP tool)
|
|
7
|
+
* to replace per-prompt rubric assembly with a deterministic, testable
|
|
8
|
+
* evaluation.
|
|
9
|
+
*
|
|
10
|
+
* Same architectural shape as `compliance-score`: read-only API calls,
|
|
11
|
+
* no state mutation, runs against a public `owner/repo` slug.
|
|
12
|
+
*/
|
|
13
|
+
import { getOctokit, requireGitHubToken } from '../core/index.js';
|
|
14
|
+
import { errorMessage } from '../core/errors.js';
|
|
15
|
+
import { warn } from '../core/logger.js';
|
|
16
|
+
import { validateRepoIdentifier } from './validation.js';
|
|
17
|
+
import { computeRepoVet } from '../core/repo-vet.js';
|
|
18
|
+
const MODULE = 'repo-vet';
|
|
19
|
+
const DAY_MS = 86400000;
|
|
20
|
+
const COMMUNITY_HEALTH_PATHS = [
|
|
21
|
+
// Each entry can live at the repo root OR under .github/. We probe both
|
|
22
|
+
// because some repos prefer the visible-at-root convention and some
|
|
23
|
+
// prefer the .github/ folder convention.
|
|
24
|
+
{ key: 'hasContributing', candidates: ['CONTRIBUTING.md', '.github/CONTRIBUTING.md'] },
|
|
25
|
+
{ key: 'hasIssueTemplates', candidates: ['.github/ISSUE_TEMPLATE'] },
|
|
26
|
+
{
|
|
27
|
+
key: 'hasPRTemplate',
|
|
28
|
+
candidates: ['.github/pull_request_template.md', 'pull_request_template.md', 'PULL_REQUEST_TEMPLATE.md'],
|
|
29
|
+
},
|
|
30
|
+
{ key: 'hasCodeOfConduct', candidates: ['CODE_OF_CONDUCT.md', '.github/CODE_OF_CONDUCT.md'] },
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Returns true when the path exists. Treats 404 as "absent" (the only
|
|
34
|
+
* shape we care about for community-health flags). Re-throws everything
|
|
35
|
+
* else — auth/rate-limit failures must not silently scrub the
|
|
36
|
+
* `hasContributing` etc. flags to `false`, which would understate a
|
|
37
|
+
* repo's community health on a token that lacks scope or has been
|
|
38
|
+
* throttled mid-sequence.
|
|
39
|
+
*/
|
|
40
|
+
async function probePath(octokit, owner, repo, path) {
|
|
41
|
+
try {
|
|
42
|
+
await octokit.repos.getContent({ owner, repo, path });
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
const status = err.status;
|
|
47
|
+
if (status === 404)
|
|
48
|
+
return false;
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Probe each community-health path. Treats 404 as "absent" (a definitive
|
|
54
|
+
* signal). On any other error (auth, rate-limit, 5xx) the per-key result
|
|
55
|
+
* stays `false` AND the whole-function `incomplete` flag is set so the
|
|
56
|
+
* caller knows community-health was unverified rather than confirmed-absent.
|
|
57
|
+
*
|
|
58
|
+
* This is the per-key analogue of the call-level catch: instead of either
|
|
59
|
+
* silently misclassifying 403 → absent OR aborting the entire repo-vet
|
|
60
|
+
* call, we degrade gracefully and surface the gap.
|
|
61
|
+
*/
|
|
62
|
+
async function checkCommunityHealth(octokit, owner, repo) {
|
|
63
|
+
const out = {
|
|
64
|
+
hasContributing: false,
|
|
65
|
+
hasIssueTemplates: false,
|
|
66
|
+
hasPRTemplate: false,
|
|
67
|
+
hasCodeOfConduct: false,
|
|
68
|
+
};
|
|
69
|
+
let incomplete = false;
|
|
70
|
+
await Promise.all(COMMUNITY_HEALTH_PATHS.map(async ({ key, candidates }) => {
|
|
71
|
+
let sawError = false;
|
|
72
|
+
for (const path of candidates) {
|
|
73
|
+
try {
|
|
74
|
+
if (await probePath(octokit, owner, repo, path)) {
|
|
75
|
+
out[key] = true;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
// probePath only throws on non-404 errors. Don't abandon
|
|
81
|
+
// remaining candidates — `CONTRIBUTING.md` failing with 403
|
|
82
|
+
// does NOT imply `.github/CONTRIBUTING.md` will fail too. Mark
|
|
83
|
+
// the per-key probe as incomplete only if every candidate
|
|
84
|
+
// either errored or returned absent.
|
|
85
|
+
warn(MODULE, `community-health probe for ${owner}/${repo}/${path} failed: ${errorMessage(err)}`);
|
|
86
|
+
sawError = true;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Reached only when no candidate hit. If any candidate errored,
|
|
91
|
+
// the absent flag is unreliable — surface that to the caller so
|
|
92
|
+
// the agent prompt distinguishes "probed all candidates and
|
|
93
|
+
// absent" from "couldn't tell".
|
|
94
|
+
if (sawError)
|
|
95
|
+
incomplete = true;
|
|
96
|
+
}));
|
|
97
|
+
return {
|
|
98
|
+
hasContributing: out.hasContributing,
|
|
99
|
+
hasIssueTemplates: out.hasIssueTemplates,
|
|
100
|
+
hasPRTemplate: out.hasPRTemplate,
|
|
101
|
+
hasCodeOfConduct: out.hasCodeOfConduct,
|
|
102
|
+
incomplete,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function summarizePRMerges(prs, windowDays, now) {
|
|
106
|
+
const cutoff = now.getTime() - windowDays * DAY_MS;
|
|
107
|
+
const prMergeTimesDays = [];
|
|
108
|
+
let mergedCount = 0;
|
|
109
|
+
let openedCount = 0;
|
|
110
|
+
for (const pr of prs) {
|
|
111
|
+
const createdAt = new Date(pr.created_at).getTime();
|
|
112
|
+
if (createdAt >= cutoff)
|
|
113
|
+
openedCount += 1;
|
|
114
|
+
if (pr.merged_at) {
|
|
115
|
+
const mergedAt = new Date(pr.merged_at).getTime();
|
|
116
|
+
if (mergedAt >= cutoff) {
|
|
117
|
+
mergedCount += 1;
|
|
118
|
+
const days = (mergedAt - createdAt) / DAY_MS;
|
|
119
|
+
if (Number.isFinite(days) && days >= 0)
|
|
120
|
+
prMergeTimesDays.push(days);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { prMergeTimesDays, mergedCount, openedCount };
|
|
125
|
+
}
|
|
126
|
+
function summarizeCommits(commits, now) {
|
|
127
|
+
const cutoff30 = now.getTime() - 30 * DAY_MS;
|
|
128
|
+
const cutoff90 = now.getTime() - 90 * DAY_MS;
|
|
129
|
+
const contributors90 = new Set();
|
|
130
|
+
let commitsLast30Days = 0;
|
|
131
|
+
let lastCommitMs = null;
|
|
132
|
+
for (const c of commits) {
|
|
133
|
+
const dateStr = c.commit.author?.date;
|
|
134
|
+
if (!dateStr)
|
|
135
|
+
continue;
|
|
136
|
+
const ts = new Date(dateStr).getTime();
|
|
137
|
+
if (Number.isNaN(ts))
|
|
138
|
+
continue;
|
|
139
|
+
if (lastCommitMs === null || ts > lastCommitMs)
|
|
140
|
+
lastCommitMs = ts;
|
|
141
|
+
if (ts >= cutoff30)
|
|
142
|
+
commitsLast30Days += 1;
|
|
143
|
+
if (ts >= cutoff90 && c.author?.login)
|
|
144
|
+
contributors90.add(c.author.login);
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
commitsLast30Days,
|
|
148
|
+
contributorsLast90d: contributors90.size,
|
|
149
|
+
lastCommitISO: lastCommitMs ? new Date(lastCommitMs).toISOString() : null,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Run the repo-vet evaluation against an `owner/repo` slug.
|
|
154
|
+
*
|
|
155
|
+
* @throws {ValidationError} If the repo identifier is malformed.
|
|
156
|
+
*/
|
|
157
|
+
export async function runRepoVet(options) {
|
|
158
|
+
validateRepoIdentifier(options.repo);
|
|
159
|
+
const [owner, repo] = options.repo.split('/');
|
|
160
|
+
const token = requireGitHubToken();
|
|
161
|
+
const octokit = getOctokit(token);
|
|
162
|
+
const now = new Date();
|
|
163
|
+
const [repoMetaResp, closedPRsResp, commitsResp, releasesResp, communityHealthSummary] = await Promise.all([
|
|
164
|
+
octokit.repos.get({ owner, repo }),
|
|
165
|
+
octokit.pulls.list({ owner, repo, state: 'closed', sort: 'updated', direction: 'desc', per_page: 100 }),
|
|
166
|
+
octokit.repos.listCommits({ owner, repo, per_page: 100 }),
|
|
167
|
+
octokit.repos
|
|
168
|
+
.listReleases({ owner, repo, per_page: 1 })
|
|
169
|
+
.catch(() => ({ data: [] })),
|
|
170
|
+
checkCommunityHealth(octokit, owner, repo),
|
|
171
|
+
]);
|
|
172
|
+
const prs = closedPRsResp.data.map((p) => ({
|
|
173
|
+
created_at: p.created_at,
|
|
174
|
+
merged_at: p.merged_at,
|
|
175
|
+
updated_at: p.updated_at,
|
|
176
|
+
}));
|
|
177
|
+
const { prMergeTimesDays, mergedCount, openedCount } = summarizePRMerges(prs, 90, now);
|
|
178
|
+
const commitSummary = summarizeCommits(commitsResp.data, now);
|
|
179
|
+
const releases = releasesResp.data;
|
|
180
|
+
const lastReleaseISO = releases.length > 0 ? (releases[0].published_at ?? releases[0].created_at ?? null) : null;
|
|
181
|
+
const input = {
|
|
182
|
+
stars: repoMetaResp.data.stargazers_count ?? 0,
|
|
183
|
+
forks: repoMetaResp.data.forks_count ?? 0,
|
|
184
|
+
openIssues: repoMetaResp.data.open_issues_count ?? 0,
|
|
185
|
+
watchers: repoMetaResp.data.subscribers_count ?? 0,
|
|
186
|
+
isArchived: repoMetaResp.data.archived ?? false,
|
|
187
|
+
lastPushed: repoMetaResp.data.pushed_at ?? new Date(0).toISOString(),
|
|
188
|
+
createdAt: repoMetaResp.data.created_at ?? new Date(0).toISOString(),
|
|
189
|
+
commitsLast30Days: commitSummary.commitsLast30Days,
|
|
190
|
+
prMergeTimesDays,
|
|
191
|
+
mergedCount90Days: mergedCount,
|
|
192
|
+
openedCount90Days: openedCount,
|
|
193
|
+
lastCommitISO: commitSummary.lastCommitISO,
|
|
194
|
+
contributorsLast90d: commitSummary.contributorsLast90d,
|
|
195
|
+
lastReleaseISO,
|
|
196
|
+
hasContributing: communityHealthSummary.hasContributing,
|
|
197
|
+
hasIssueTemplates: communityHealthSummary.hasIssueTemplates,
|
|
198
|
+
hasPRTemplate: communityHealthSummary.hasPRTemplate,
|
|
199
|
+
hasCodeOfConduct: communityHealthSummary.hasCodeOfConduct,
|
|
200
|
+
};
|
|
201
|
+
const result = computeRepoVet(input);
|
|
202
|
+
// The core function names its metadata object `repo`. Rename to `repoMeta`
|
|
203
|
+
// at the CLI boundary so the top-level slug doesn't collide with it.
|
|
204
|
+
// Also overlay the community-health `incomplete` flag the wrapper
|
|
205
|
+
// tracks (the core type doesn't carry it because computeRepoVet is
|
|
206
|
+
// pure — only the wrapper makes the API calls that can fail mid-probe).
|
|
207
|
+
const { repo: repoMeta, communityHealth, ...rest } = result;
|
|
208
|
+
return {
|
|
209
|
+
repoSlug: options.repo,
|
|
210
|
+
fetchedAt: now.toISOString(),
|
|
211
|
+
repoMeta,
|
|
212
|
+
communityHealth: { ...communityHealth, incomplete: communityHealthSummary.incomplete },
|
|
213
|
+
...rest,
|
|
214
|
+
};
|
|
215
|
+
}
|
package/dist/commands/startup.js
CHANGED
|
@@ -277,6 +277,22 @@ export async function runStartup() {
|
|
|
277
277
|
}
|
|
278
278
|
// 5. Detect issue list
|
|
279
279
|
const issueList = detectIssueList();
|
|
280
|
+
// 6. Pass through dashboard-build status set by the workflow shell (#1293).
|
|
281
|
+
// The workflow runs the SPA build BEFORE invoking startup, so the status
|
|
282
|
+
// arrives here via env vars rather than being something runStartup itself
|
|
283
|
+
// computes. Validate the env value so a malformed export can't sneak an
|
|
284
|
+
// arbitrary string into a typed enum.
|
|
285
|
+
const rawBuildStatus = process.env.OSS_DASHBOARD_BUILD_STATUS;
|
|
286
|
+
const dashboardBuildStatus = rawBuildStatus === 'fresh' ||
|
|
287
|
+
rawBuildStatus === 'rebuilt' ||
|
|
288
|
+
rawBuildStatus === 'failed' ||
|
|
289
|
+
rawBuildStatus === 'missing-pnpm'
|
|
290
|
+
? rawBuildStatus
|
|
291
|
+
: undefined;
|
|
292
|
+
const rawBuildErrorTail = process.env.OSS_DASHBOARD_BUILD_ERROR_TAIL;
|
|
293
|
+
const dashboardBuildErrorTail = (dashboardBuildStatus === 'failed' || dashboardBuildStatus === 'missing-pnpm') && rawBuildErrorTail
|
|
294
|
+
? rawBuildErrorTail
|
|
295
|
+
: undefined;
|
|
280
296
|
return {
|
|
281
297
|
version,
|
|
282
298
|
setupComplete: true,
|
|
@@ -284,6 +300,8 @@ export async function runStartup() {
|
|
|
284
300
|
daily,
|
|
285
301
|
dashboardUrl,
|
|
286
302
|
dashboardError,
|
|
303
|
+
dashboardBuildStatus,
|
|
304
|
+
dashboardBuildErrorTail,
|
|
287
305
|
issueList,
|
|
288
306
|
};
|
|
289
307
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI-enforced tool detection (#1286).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `workflows/pre-commit-review.md` Steps 2b and 6a so
|
|
5
|
+
* the same logic isn't reimplemented twice in markdown. Callers
|
|
6
|
+
* supply pre-fetched config snippets (the workflow runs `cat` /
|
|
7
|
+
* `head` against the well-known files); this function does the
|
|
8
|
+
* structured parse.
|
|
9
|
+
*
|
|
10
|
+
* Pure typed helper — no I/O.
|
|
11
|
+
*/
|
|
12
|
+
export type CIToolName = 'lint' | 'format' | 'typecheck' | 'test' | 'build' | 'commit-format' | 'security-scan';
|
|
13
|
+
export interface CIEnforcedToolsInput {
|
|
14
|
+
/** `.pre-commit-config.yaml` contents, or null when the file is missing. */
|
|
15
|
+
preCommitConfigYaml: string | null;
|
|
16
|
+
/** Concatenated contents of `.github/workflows/*.yml` (or null when none). */
|
|
17
|
+
workflowYamls: string | null;
|
|
18
|
+
/** `Makefile` contents, or null when missing. */
|
|
19
|
+
makefile: string | null;
|
|
20
|
+
/** `package.json` contents — used to detect script names. */
|
|
21
|
+
packageJson: string | null;
|
|
22
|
+
}
|
|
23
|
+
export interface CIEnforcedTool {
|
|
24
|
+
tool: CIToolName;
|
|
25
|
+
source: 'pre-commit' | 'github-workflow' | 'makefile' | 'package-json';
|
|
26
|
+
/** Short snippet that surfaced this tool — useful for explainability. */
|
|
27
|
+
evidence: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Detect which class of tool each input source enforces. The output
|
|
31
|
+
* may contain duplicates (e.g., `test` enforced by both pre-commit
|
|
32
|
+
* and a GitHub workflow); callers can dedupe on `tool` or display
|
|
33
|
+
* each evidence pair separately.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getCIEnforcedTools(input: CIEnforcedToolsInput): CIEnforcedTool[];
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI-enforced tool detection (#1286).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `workflows/pre-commit-review.md` Steps 2b and 6a so
|
|
5
|
+
* the same logic isn't reimplemented twice in markdown. Callers
|
|
6
|
+
* supply pre-fetched config snippets (the workflow runs `cat` /
|
|
7
|
+
* `head` against the well-known files); this function does the
|
|
8
|
+
* structured parse.
|
|
9
|
+
*
|
|
10
|
+
* Pure typed helper — no I/O.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Detect which class of tool each input source enforces. The output
|
|
14
|
+
* may contain duplicates (e.g., `test` enforced by both pre-commit
|
|
15
|
+
* and a GitHub workflow); callers can dedupe on `tool` or display
|
|
16
|
+
* each evidence pair separately.
|
|
17
|
+
*/
|
|
18
|
+
export function getCIEnforcedTools(input) {
|
|
19
|
+
const out = [];
|
|
20
|
+
if (input.preCommitConfigYaml) {
|
|
21
|
+
detectFromHaystack(input.preCommitConfigYaml, 'pre-commit', out);
|
|
22
|
+
}
|
|
23
|
+
if (input.workflowYamls) {
|
|
24
|
+
detectFromHaystack(input.workflowYamls, 'github-workflow', out);
|
|
25
|
+
}
|
|
26
|
+
if (input.makefile) {
|
|
27
|
+
detectFromHaystack(input.makefile, 'makefile', out);
|
|
28
|
+
}
|
|
29
|
+
if (input.packageJson) {
|
|
30
|
+
detectScriptsFromPackageJson(input.packageJson, out);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
function detectFromHaystack(haystack, source, out) {
|
|
35
|
+
const lower = haystack.toLowerCase();
|
|
36
|
+
const checks = [
|
|
37
|
+
{
|
|
38
|
+
tool: 'lint',
|
|
39
|
+
pattern: /\b(?:eslint|biome|ruff|flake8|rubocop|golangci-lint|clippy|pylint)\b/i,
|
|
40
|
+
evidenceFor: ['eslint', 'biome', 'ruff', 'flake8', 'rubocop', 'golangci-lint', 'clippy', 'pylint'],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
tool: 'format',
|
|
44
|
+
pattern: /\b(?:prettier|biome[\s-]+format|black|gofmt|rustfmt|clang-format)\b/i,
|
|
45
|
+
evidenceFor: ['prettier', 'biome', 'black', 'gofmt', 'rustfmt', 'clang-format'],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
tool: 'typecheck',
|
|
49
|
+
pattern: /\b(?:tsc|mypy|sorbet|pyright|flow)\b/i,
|
|
50
|
+
evidenceFor: ['tsc', 'mypy', 'sorbet', 'pyright', 'flow'],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
tool: 'test',
|
|
54
|
+
pattern: /\b(?:vitest|jest|pytest|rspec|go test|cargo test|mocha)\b/i,
|
|
55
|
+
evidenceFor: ['vitest', 'jest', 'pytest', 'rspec', 'go test', 'cargo test', 'mocha'],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
// `tsc` alone (or `tsc --noEmit`) is a typecheck, not a build artifact
|
|
59
|
+
// step — keep it out of this pattern so a workflow with only
|
|
60
|
+
// `tsc --noEmit` doesn't fire BOTH `typecheck` and `build`. Real
|
|
61
|
+
// build steps that do produce artifacts (tsup, esbuild, webpack,
|
|
62
|
+
// cargo build, go build) stay here.
|
|
63
|
+
tool: 'build',
|
|
64
|
+
pattern: /\b(?:tsup|esbuild|webpack|cargo build|go build)\b/i,
|
|
65
|
+
evidenceFor: ['tsup', 'esbuild', 'webpack', 'cargo build', 'go build'],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
tool: 'commit-format',
|
|
69
|
+
pattern: /\b(?:commitlint|conventional-commits)\b/i,
|
|
70
|
+
evidenceFor: ['commitlint', 'conventional-commits'],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
tool: 'security-scan',
|
|
74
|
+
pattern: /\b(?:codeql|semgrep|trivy|gitleaks|snyk)\b/i,
|
|
75
|
+
evidenceFor: ['codeql', 'semgrep', 'trivy', 'gitleaks', 'snyk'],
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
for (const c of checks) {
|
|
79
|
+
if (c.pattern.test(lower)) {
|
|
80
|
+
const evidence = c.evidenceFor.find((e) => lower.includes(e)) ?? c.tool;
|
|
81
|
+
out.push({ tool: c.tool, source, evidence });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function detectScriptsFromPackageJson(packageJson, out) {
|
|
86
|
+
let parsed;
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(packageJson);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const scripts = parsed.scripts ?? {};
|
|
94
|
+
const scriptToTool = [
|
|
95
|
+
{ key: /^lint(:|$)/, tool: 'lint' },
|
|
96
|
+
{ key: /^format(:|$)/, tool: 'format' },
|
|
97
|
+
{ key: /^typecheck(:|$)|^tsc(:|$)/, tool: 'typecheck' },
|
|
98
|
+
{ key: /^test(:|$)/, tool: 'test' },
|
|
99
|
+
{ key: /^build(:|$)/, tool: 'build' },
|
|
100
|
+
];
|
|
101
|
+
for (const [name] of Object.entries(scripts)) {
|
|
102
|
+
for (const m of scriptToTool) {
|
|
103
|
+
if (m.key.test(name)) {
|
|
104
|
+
out.push({ tool: m.tool, source: 'package-json', evidence: `scripts.${name}` });
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-push comment decision (#1286).
|
|
3
|
+
*
|
|
4
|
+
* Centralizes the "should I draft a comment, or does the diff speak
|
|
5
|
+
* for itself?" rule that was duplicated between
|
|
6
|
+
* `workflows/pre-commit-review.md` Step 7a and `agents/pr-responder.md`
|
|
7
|
+
* Step 3.
|
|
8
|
+
*
|
|
9
|
+
* Pure typed helper — no LLM, no I/O. Callers do the upstream natural-
|
|
10
|
+
* language classification (mapping a maintainer comment to a structured
|
|
11
|
+
* `FeedbackCategory` + flags) and pass in the result. This module owns
|
|
12
|
+
* the rule that decides skip vs. draft from the structured input.
|
|
13
|
+
*
|
|
14
|
+
* Same architectural shape as #1245 (compliance-score), #1242 (repo-vet),
|
|
15
|
+
* #1243 (strategy), #1252 (pr-quality-rubric), and #1264 (issue-effort /
|
|
16
|
+
* maintainer-hints).
|
|
17
|
+
*/
|
|
18
|
+
export type FeedbackCategory = 'code_request' | 'question' | 'explanation_request' | 'style_request' | 'design_discussion' | 'approval_with_nit' | 'formatting_complaint';
|
|
19
|
+
export interface FeedbackClassification {
|
|
20
|
+
category: FeedbackCategory;
|
|
21
|
+
/**
|
|
22
|
+
* Whether this individual feedback item, considered in isolation,
|
|
23
|
+
* requires a drafted comment. Aggregated by `shouldDraftResponse`
|
|
24
|
+
* across the full feedback list.
|
|
25
|
+
*/
|
|
26
|
+
needsComment: boolean;
|
|
27
|
+
/** Short rationale rendered alongside the decision. */
|
|
28
|
+
reason: string;
|
|
29
|
+
}
|
|
30
|
+
export interface CommentDecisionInput {
|
|
31
|
+
/**
|
|
32
|
+
* Pre-classified feedback items in chronological order. The caller
|
|
33
|
+
* (agent or workflow) is responsible for the natural-language
|
|
34
|
+
* categorization step; this function only consumes structured input.
|
|
35
|
+
*/
|
|
36
|
+
feedback: readonly FeedbackClassification[];
|
|
37
|
+
/**
|
|
38
|
+
* True when the contributor pushed code that addresses every
|
|
39
|
+
* feedback item that requested a code change. The agent's diff
|
|
40
|
+
* inspection determines this; the function trusts the boolean.
|
|
41
|
+
*/
|
|
42
|
+
allRequestedChangesAddressed: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface CommentDecisionResult {
|
|
45
|
+
shouldDraft: boolean;
|
|
46
|
+
reason: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Decide whether the contributor should draft a comment after a push.
|
|
50
|
+
*
|
|
51
|
+
* Rule (sourced verbatim from `workflows/pre-commit-review.md` Step 7a
|
|
52
|
+
* and `agents/pr-responder.md` "Comment Decision Logic"):
|
|
53
|
+
*
|
|
54
|
+
* - Skip the comment entirely when ALL of these are true:
|
|
55
|
+
* 1. Every piece of maintainer feedback that requested code changes
|
|
56
|
+
* has a corresponding code change.
|
|
57
|
+
* 2. No question was asked.
|
|
58
|
+
* 3. Nothing was intentionally left unchanged (a "yes the maintainer
|
|
59
|
+
* asked but I did not change it" item always needs explanation).
|
|
60
|
+
* 4. The diff makes the fix self-evident — modeled here as the
|
|
61
|
+
* `allRequestedChangesAddressed` flag the caller passes in.
|
|
62
|
+
*
|
|
63
|
+
* - Draft a comment if ANY of these are true:
|
|
64
|
+
* 1. The maintainer asked a question.
|
|
65
|
+
* 2. The feedback is conceptual or design-level (the diff alone
|
|
66
|
+
* cannot answer "why").
|
|
67
|
+
* 3. Something was intentionally left unchanged (`needsComment: true`
|
|
68
|
+
* on a code-request item where the contributor disagrees).
|
|
69
|
+
* 4. Only some of multiple requested changes were addressed.
|
|
70
|
+
* 5. The contributor deviated from exactly what was asked.
|
|
71
|
+
*/
|
|
72
|
+
export declare function shouldDraftResponse(input: CommentDecisionInput): CommentDecisionResult;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-push comment decision (#1286).
|
|
3
|
+
*
|
|
4
|
+
* Centralizes the "should I draft a comment, or does the diff speak
|
|
5
|
+
* for itself?" rule that was duplicated between
|
|
6
|
+
* `workflows/pre-commit-review.md` Step 7a and `agents/pr-responder.md`
|
|
7
|
+
* Step 3.
|
|
8
|
+
*
|
|
9
|
+
* Pure typed helper — no LLM, no I/O. Callers do the upstream natural-
|
|
10
|
+
* language classification (mapping a maintainer comment to a structured
|
|
11
|
+
* `FeedbackCategory` + flags) and pass in the result. This module owns
|
|
12
|
+
* the rule that decides skip vs. draft from the structured input.
|
|
13
|
+
*
|
|
14
|
+
* Same architectural shape as #1245 (compliance-score), #1242 (repo-vet),
|
|
15
|
+
* #1243 (strategy), #1252 (pr-quality-rubric), and #1264 (issue-effort /
|
|
16
|
+
* maintainer-hints).
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Decide whether the contributor should draft a comment after a push.
|
|
20
|
+
*
|
|
21
|
+
* Rule (sourced verbatim from `workflows/pre-commit-review.md` Step 7a
|
|
22
|
+
* and `agents/pr-responder.md` "Comment Decision Logic"):
|
|
23
|
+
*
|
|
24
|
+
* - Skip the comment entirely when ALL of these are true:
|
|
25
|
+
* 1. Every piece of maintainer feedback that requested code changes
|
|
26
|
+
* has a corresponding code change.
|
|
27
|
+
* 2. No question was asked.
|
|
28
|
+
* 3. Nothing was intentionally left unchanged (a "yes the maintainer
|
|
29
|
+
* asked but I did not change it" item always needs explanation).
|
|
30
|
+
* 4. The diff makes the fix self-evident — modeled here as the
|
|
31
|
+
* `allRequestedChangesAddressed` flag the caller passes in.
|
|
32
|
+
*
|
|
33
|
+
* - Draft a comment if ANY of these are true:
|
|
34
|
+
* 1. The maintainer asked a question.
|
|
35
|
+
* 2. The feedback is conceptual or design-level (the diff alone
|
|
36
|
+
* cannot answer "why").
|
|
37
|
+
* 3. Something was intentionally left unchanged (`needsComment: true`
|
|
38
|
+
* on a code-request item where the contributor disagrees).
|
|
39
|
+
* 4. Only some of multiple requested changes were addressed.
|
|
40
|
+
* 5. The contributor deviated from exactly what was asked.
|
|
41
|
+
*/
|
|
42
|
+
export function shouldDraftResponse(input) {
|
|
43
|
+
if (input.feedback.length === 0) {
|
|
44
|
+
return { shouldDraft: false, reason: 'no maintainer feedback to address' };
|
|
45
|
+
}
|
|
46
|
+
// Categories that always demand a written reply, regardless of the
|
|
47
|
+
// diff. The diff cannot answer a question or carry a design decision.
|
|
48
|
+
const alwaysDraftCategories = new Set(['question', 'explanation_request', 'design_discussion']);
|
|
49
|
+
for (const item of input.feedback) {
|
|
50
|
+
if (alwaysDraftCategories.has(item.category)) {
|
|
51
|
+
return {
|
|
52
|
+
shouldDraft: true,
|
|
53
|
+
reason: `${item.category.replace(/_/g, ' ')} — diff alone cannot address this`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Caller flagged at least one item as "I'm intentionally not making
|
|
58
|
+
// this change" or "I deviated" — needs an explanation either way.
|
|
59
|
+
const intentionalGap = input.feedback.find((item) => item.needsComment);
|
|
60
|
+
if (intentionalGap) {
|
|
61
|
+
return {
|
|
62
|
+
shouldDraft: true,
|
|
63
|
+
reason: `requires written explanation: ${intentionalGap.reason}`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// Caller signaled the diff covers everything that was requested.
|
|
67
|
+
if (input.allRequestedChangesAddressed) {
|
|
68
|
+
return { shouldDraft: false, reason: 'all requested changes are visible in the diff' };
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
shouldDraft: true,
|
|
72
|
+
reason: 'not all requested changes are visible in the diff yet',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR compliance scoring (#1245).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `agents/pr-compliance-checker.md`'s in-prompt scoring
|
|
5
|
+
* tables so the weights, thresholds, and per-check rules are
|
|
6
|
+
* deterministic, unit-testable, and tunable without editing markdown.
|
|
7
|
+
* Same architectural shape as success-grade (#858), linked-PR
|
|
8
|
+
* classifier (#910), and anti-AI scan (#911).
|
|
9
|
+
*
|
|
10
|
+
* The function intentionally does not fetch PR data — callers (the MCP
|
|
11
|
+
* tool, the CLI command, the agent) supply pre-fetched metadata so the
|
|
12
|
+
* score is reproducible against fixture data and the same input shape
|
|
13
|
+
* works for both live PRs and historical replay.
|
|
14
|
+
*/
|
|
15
|
+
export type ComplianceCheckStatus = 'pass' | 'warn' | 'fail';
|
|
16
|
+
export interface ComplianceCheckResult {
|
|
17
|
+
status: ComplianceCheckStatus;
|
|
18
|
+
weight: number;
|
|
19
|
+
detail: string;
|
|
20
|
+
}
|
|
21
|
+
export type ComplianceRating = 'ready' | 'minor' | 'fix_first' | 'significant_work';
|
|
22
|
+
/** Emoji surfaced alongside the rating in agent output. */
|
|
23
|
+
export type ComplianceEmoji = '🌟' | '✅' | '⚠️' | '❌';
|
|
24
|
+
export interface ComplianceScoreResult {
|
|
25
|
+
/** 0–100 weighted score across the six checks. */
|
|
26
|
+
score: number;
|
|
27
|
+
rating: ComplianceRating;
|
|
28
|
+
emoji: ComplianceEmoji;
|
|
29
|
+
checks: {
|
|
30
|
+
issueReference: ComplianceCheckResult;
|
|
31
|
+
description: ComplianceCheckResult;
|
|
32
|
+
focusedChanges: ComplianceCheckResult;
|
|
33
|
+
tests: ComplianceCheckResult;
|
|
34
|
+
title: ComplianceCheckResult;
|
|
35
|
+
branch: ComplianceCheckResult;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Minimum PR metadata required to compute a compliance score. */
|
|
39
|
+
export interface PRMetadata {
|
|
40
|
+
title: string;
|
|
41
|
+
body: string;
|
|
42
|
+
branch: string;
|
|
43
|
+
filesChangedCount: number;
|
|
44
|
+
additions: number;
|
|
45
|
+
deletions: number;
|
|
46
|
+
/**
|
|
47
|
+
* Filenames touched by the PR. Used by the test-detection check to
|
|
48
|
+
* decide whether the PR includes a test file.
|
|
49
|
+
*/
|
|
50
|
+
files: string[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Verified state of an issue referenced from a PR body. Populated by
|
|
54
|
+
* the compliance-score command (which calls the Issues API per
|
|
55
|
+
* reference) and consumed by `checkIssueReference` to fail loud on
|
|
56
|
+
* broken links (#1246 Improvement B).
|
|
57
|
+
*/
|
|
58
|
+
export interface LinkedIssueInfo {
|
|
59
|
+
/** Issue number parsed from the PR body. */
|
|
60
|
+
number: number;
|
|
61
|
+
/** Owner/repo where the issue lives — may differ from the PR's repo
|
|
62
|
+
* when a cross-repo reference like `owner/other#42` is used. */
|
|
63
|
+
repo: string;
|
|
64
|
+
/** True when the reference targeted a different repo than the PR. */
|
|
65
|
+
crossRepo: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Result of the verification API call.
|
|
68
|
+
* - `open` / `closed`: confirmed live state.
|
|
69
|
+
* - `not_found`: HTTP 404 — the referenced issue does not exist.
|
|
70
|
+
* - `unverifiable`: a non-404 failure (rate-limit, 5xx, network).
|
|
71
|
+
* The check treats this neutrally rather than as a broken link, so a
|
|
72
|
+
* GitHub API hiccup doesn't downgrade a valid PR's compliance score.
|
|
73
|
+
*/
|
|
74
|
+
state: 'open' | 'closed' | 'not_found' | 'unverifiable';
|
|
75
|
+
/** Whole days since the issue was closed, when state === 'closed'.
|
|
76
|
+
* Used to distinguish "recently closed, may still apply" from "long
|
|
77
|
+
* stale, almost certainly the wrong reference." */
|
|
78
|
+
closedDaysAgo?: number;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Optional repo context used to fine-tune individual check thresholds
|
|
82
|
+
* (#1245). All fields are optional; absent fields use safe defaults that
|
|
83
|
+
* match the original in-prompt rules.
|
|
84
|
+
*/
|
|
85
|
+
export interface RepoContext {
|
|
86
|
+
/**
|
|
87
|
+
* Whether the target repo has any visible test infrastructure
|
|
88
|
+
* (`test/`, `tests/`, `__tests__/`, `spec/`, etc.). When `false`, the
|
|
89
|
+
* tests check downgrades from `fail` to `warn` because tests aren't
|
|
90
|
+
* required by the project.
|
|
91
|
+
*/
|
|
92
|
+
hasTestInfrastructure?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Verified state of every issue/PR reference found in the PR body
|
|
95
|
+
* (#1246 Improvement B). When provided, `checkIssueReference` will
|
|
96
|
+
* fail-loud on broken or stale references rather than passing on the
|
|
97
|
+
* regex match alone. Absent / empty array preserves original
|
|
98
|
+
* regex-only behavior.
|
|
99
|
+
*/
|
|
100
|
+
linkedIssues?: LinkedIssueInfo[];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* After how many days a closed-issue reference flips from "warn"
|
|
104
|
+
* (probably still relevant) to "fail" (probably stale). Exported so
|
|
105
|
+
* callers can document the cutoff (#1246).
|
|
106
|
+
*/
|
|
107
|
+
export declare const CLOSED_ISSUE_RECENT_DAYS = 30;
|
|
108
|
+
/** Title byte budget — Conventional Commits style fits comfortably under 72. */
|
|
109
|
+
export { TITLE_LENGTH_BUDGET } from './pr-quality-rubric.js';
|
|
110
|
+
/** "Focused changes" thresholds. Source of truth lives in pr-quality-rubric.ts. */
|
|
111
|
+
export declare const FOCUSED_CHANGES: {
|
|
112
|
+
readonly passFiles: 10;
|
|
113
|
+
readonly passLines: 400;
|
|
114
|
+
readonly warnFiles: 20;
|
|
115
|
+
readonly warnLines: 800;
|
|
116
|
+
};
|
|
117
|
+
/** Score → rating cutoffs. */
|
|
118
|
+
export declare const RATING_CUTOFFS: {
|
|
119
|
+
readonly ready: 90;
|
|
120
|
+
readonly minor: 75;
|
|
121
|
+
readonly fixFirst: 60;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Compute a compliance score from PR metadata, optionally fine-tuned by
|
|
125
|
+
* repo context (#1245). Pure function — no I/O, no global state.
|
|
126
|
+
*/
|
|
127
|
+
export declare function computeComplianceScore(meta: PRMetadata, repoContext?: RepoContext): ComplianceScoreResult;
|