@reneturcios/greenlight 0.0.0-stage → 0.3.2
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 +213 -2
- package/dist/automate.d.ts +184 -0
- package/dist/automate.js +637 -0
- package/dist/automate.js.map +1 -0
- package/dist/claims.d.ts +20 -0
- package/dist/claims.js +112 -0
- package/dist/claims.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +337 -0
- package/dist/cli.js.map +1 -0
- package/dist/friction-index.d.ts +1 -0
- package/dist/friction-index.js +152 -0
- package/dist/friction-index.js.map +1 -0
- package/dist/friction.d.ts +28 -0
- package/dist/friction.js +54 -0
- package/dist/friction.js.map +1 -0
- package/dist/gates.d.ts +51 -0
- package/dist/gates.js +198 -0
- package/dist/gates.js.map +1 -0
- package/dist/github.d.ts +155 -0
- package/dist/github.js +531 -0
- package/dist/github.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +173 -0
- package/dist/index.js.map +1 -0
- package/dist/linkstrength.d.ts +27 -0
- package/dist/linkstrength.js +25 -0
- package/dist/linkstrength.js.map +1 -0
- package/dist/types.d.ts +131 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/verdict.d.ts +64 -0
- package/dist/verdict.js +220 -0
- package/dist/verdict.js.map +1 -0
- package/package.json +39 -3
- package/server.json +28 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Friction index generator: `npm run index`.
|
|
3
|
+
*
|
|
4
|
+
* Builds data/friction-index.json for the repos we hand-vetted on
|
|
5
|
+
* 2026-10-13. For each repo it tries the live CONTRIBUTING.md parse first;
|
|
6
|
+
* when the API is unreachable or rate-limited it falls back to the
|
|
7
|
+
* hand-verified gate list below and labels the entry accordingly.
|
|
8
|
+
*
|
|
9
|
+
* Rule: fields we cannot compute live stay null with a note. We never
|
|
10
|
+
* fabricate numbers.
|
|
11
|
+
*/
|
|
12
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { parseGates, hardGates } from "./gates.js";
|
|
16
|
+
import { scoreFriction } from "./friction.js";
|
|
17
|
+
import { fetchContributing } from "./github.js";
|
|
18
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
|
+
function gate(kind, evidence) {
|
|
20
|
+
return { kind, detected: true, evidence };
|
|
21
|
+
}
|
|
22
|
+
const HAND = [
|
|
23
|
+
{
|
|
24
|
+
owner: "PrefectHQ", repo: "fastmcp",
|
|
25
|
+
gates: [gate("assignment_required", "hand-verified 2026-10-13: issue must be assigned or carry a 'prs welcome' label (PrefectHQ/fastmcp PR #4557)")],
|
|
26
|
+
note: "hand-verified 2026-10-13",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
owner: "modelcontextprotocol", repo: "inspector",
|
|
30
|
+
gates: [gate("collaborators_only", "hand-verified 2026-10-15: AGENTS.md states PRs are opened by repo maintainers only; outsiders file detailed issues (verified while working #2723)")],
|
|
31
|
+
note: "hand-verified 2026-10-15",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
owner: "modelcontextprotocol", repo: "servers",
|
|
35
|
+
gates: [gate("collaborators_only", "hand-verified 2026-10-15: CONTRIBUTING.md states 'We accept issues, not pull requests'; outsiders share the prompt behind a local fix (verified while working #5153)")],
|
|
36
|
+
note: "hand-verified 2026-10-15",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
owner: "triggerdotdev", repo: "trigger.dev",
|
|
40
|
+
gates: [gate("vouch_required", "hand-verified 2026-10-13: unvouched-author PRs auto-closed; vouch-request issue required first")],
|
|
41
|
+
note: "hand-verified 2026-10-13",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
owner: "langchain-ai", repo: "langchain",
|
|
45
|
+
gates: [],
|
|
46
|
+
note: "hand-verified 2026-10-13: no codified gate found, but reporters routinely request assignment before coding",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
owner: "langchain-ai", repo: "deepagents",
|
|
50
|
+
gates: [],
|
|
51
|
+
note: "hand-verified 2026-10-13: no codified gate found; assignment-request culture in comments",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
owner: "openclaw", repo: "openclaw",
|
|
55
|
+
gates: [],
|
|
56
|
+
note: "hand-verified 2026-10-13: heavy bot triage (ClawSweeper) is advisory only; no hard pre-PR gate",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
owner: "batrapulkit", repo: "squidbrake",
|
|
60
|
+
gates: [gate("claim_first", "hand-verified 2026-10-14: maintainer asks contributors to comment to claim before starting")],
|
|
61
|
+
note: "hand-verified 2026-10-13",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
owner: "reticlehq", repo: "reticle",
|
|
65
|
+
gates: [],
|
|
66
|
+
note: "hand-verified 2026-10-13: PR policy 'all', no pre-PR gate",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
owner: "shadcn-labs", repo: "pdfcn",
|
|
70
|
+
gates: [],
|
|
71
|
+
note: "hand-verified 2026-10-13: no formal gate, but good-first-issues are claimed within hours (claim speed is the game)",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
owner: "GustavoBaranda", repo: "chatflow-agent",
|
|
75
|
+
gates: [],
|
|
76
|
+
note: "hand-verified 2026-10-13: no pre-PR gate observed",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
owner: "modelcontextprotocol", repo: "typescript-sdk",
|
|
80
|
+
gates: [gate("assignment_required", "hand-verified 2026-10-13: SDKs expect a linked assigned or 'help wanted' issue, or an assignment comment")],
|
|
81
|
+
note: "hand-verified 2026-10-13",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
owner: "modelcontextprotocol", repo: "python-sdk",
|
|
85
|
+
gates: [gate("assignment_required", "hand-verified 2026-10-13: same assignment culture as the other MCP SDKs")],
|
|
86
|
+
note: "hand-verified 2026-10-13",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
owner: "modelcontextprotocol", repo: "servers",
|
|
90
|
+
gates: [gate("collaborators_only", "hand-verified 2026-10-13: PR creation policy collaborators_only")],
|
|
91
|
+
note: "hand-verified 2026-10-13",
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
/** Merge live-parsed gates with hand-verified gates: hand data wins on kind, live can add new kinds. */
|
|
95
|
+
function mergeGates(hand, live) {
|
|
96
|
+
const byKind = new Map();
|
|
97
|
+
for (const g of live)
|
|
98
|
+
byKind.set(g.kind, g);
|
|
99
|
+
for (const g of hand)
|
|
100
|
+
byKind.set(g.kind, g); // hand-verified takes precedence
|
|
101
|
+
const all = [];
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
for (const g of [...hand, ...live]) {
|
|
104
|
+
if (seen.has(g.kind))
|
|
105
|
+
continue;
|
|
106
|
+
seen.add(g.kind);
|
|
107
|
+
all.push(byKind.get(g.kind));
|
|
108
|
+
}
|
|
109
|
+
return all;
|
|
110
|
+
}
|
|
111
|
+
async function main() {
|
|
112
|
+
const reports = [];
|
|
113
|
+
let liveOk = 0;
|
|
114
|
+
let liveFailed = 0;
|
|
115
|
+
for (const entry of HAND) {
|
|
116
|
+
let contributing;
|
|
117
|
+
let liveNote = entry.note;
|
|
118
|
+
try {
|
|
119
|
+
contributing = await fetchContributing(entry.owner, entry.repo);
|
|
120
|
+
liveOk++;
|
|
121
|
+
liveNote = "live 2026-10-13, merged with hand-verified gates";
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
liveFailed++;
|
|
125
|
+
contributing = undefined;
|
|
126
|
+
}
|
|
127
|
+
const liveGates = parseGates(contributing);
|
|
128
|
+
const gates = mergeGates(entry.gates, liveGates);
|
|
129
|
+
const report = scoreFriction({
|
|
130
|
+
owner: entry.owner,
|
|
131
|
+
repo: entry.repo,
|
|
132
|
+
gates,
|
|
133
|
+
externalPRMergeRate: null,
|
|
134
|
+
medianFirstReviewHours: null,
|
|
135
|
+
maintainerIssueResponseHours: null,
|
|
136
|
+
openGoodFirstIssueCount: null,
|
|
137
|
+
note: liveNote,
|
|
138
|
+
});
|
|
139
|
+
// Surface the hard gates in reasons even when score math already did.
|
|
140
|
+
const hard = hardGates(gates);
|
|
141
|
+
if (hard.length > 0)
|
|
142
|
+
report.reasons.unshift(`HARD GATE: ${hard.map((g) => g.kind).join(", ")}`);
|
|
143
|
+
reports.push(report);
|
|
144
|
+
}
|
|
145
|
+
reports.sort((a, b) => b.score - a.score);
|
|
146
|
+
const outDir = join(ROOT, "data");
|
|
147
|
+
mkdirSync(outDir, { recursive: true });
|
|
148
|
+
writeFileSync(join(outDir, "friction-index.json"), JSON.stringify({ generated: new Date().toISOString(), repos: reports }, null, 2), "utf8");
|
|
149
|
+
console.log(`wrote data/friction-index.json with ${reports.length} repos (live fetch ok: ${liveOk}, fallback: ${liveFailed})`);
|
|
150
|
+
}
|
|
151
|
+
await main();
|
|
152
|
+
//# sourceMappingURL=friction-index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"friction-index.js","sourceRoot":"","sources":["../src/friction-index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo friction score: 0-100, where 100 = easiest place for an outsider
|
|
3
|
+
* to land a PR.
|
|
4
|
+
*
|
|
5
|
+
* Formula (documented, deterministic):
|
|
6
|
+
* start at 100
|
|
7
|
+
* gates:
|
|
8
|
+
* assignment_required -25
|
|
9
|
+
* vouch_required -35
|
|
10
|
+
* collaborators_only -50
|
|
11
|
+
* discussion_first -10
|
|
12
|
+
* claim_first -5
|
|
13
|
+
* cla_required -5
|
|
14
|
+
* dco_required -5
|
|
15
|
+
* outcome signals (only when computed, never guessed):
|
|
16
|
+
* external PR merge rate: +(rate - 0.5) * 40
|
|
17
|
+
* (a 100% external merge rate adds 20; 0% subtracts 20)
|
|
18
|
+
* median hours to first review: -min(hours / 24, 14)
|
|
19
|
+
* (a week of waiting costs 7 points, capped at 14)
|
|
20
|
+
* maintainer hours to first issue response: -min(hours / 48, 10)
|
|
21
|
+
* opportunity:
|
|
22
|
+
* open good-first-issue count: +min(count, 20) * 0.5 (max +10)
|
|
23
|
+
* clamp to [0, 100], rounded.
|
|
24
|
+
*
|
|
25
|
+
* Null signals are simply skipped: the index never fabricates numbers.
|
|
26
|
+
*/
|
|
27
|
+
import type { FrictionReport, RepoSignals } from "./types.js";
|
|
28
|
+
export declare function scoreFriction(signals: RepoSignals): FrictionReport;
|
package/dist/friction.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { gateLabel } from "./gates.js";
|
|
2
|
+
const GATE_WEIGHTS = {
|
|
3
|
+
assignment_required: -25,
|
|
4
|
+
vouch_required: -35,
|
|
5
|
+
collaborators_only: -50,
|
|
6
|
+
discussion_first: -10,
|
|
7
|
+
claim_first: -5,
|
|
8
|
+
cla_required: -5,
|
|
9
|
+
dco_required: -5,
|
|
10
|
+
};
|
|
11
|
+
function clamp(n, lo, hi) {
|
|
12
|
+
return Math.max(lo, Math.min(hi, n));
|
|
13
|
+
}
|
|
14
|
+
export function scoreFriction(signals) {
|
|
15
|
+
let score = 100;
|
|
16
|
+
const reasons = [];
|
|
17
|
+
for (const gate of signals.gates) {
|
|
18
|
+
if (!gate.detected)
|
|
19
|
+
continue;
|
|
20
|
+
const w = GATE_WEIGHTS[gate.kind] ?? 0;
|
|
21
|
+
score += w;
|
|
22
|
+
reasons.push(`${gateLabel(gate.kind)} (${w})`);
|
|
23
|
+
}
|
|
24
|
+
if (signals.externalPRMergeRate !== undefined && signals.externalPRMergeRate !== null) {
|
|
25
|
+
const delta = (signals.externalPRMergeRate - 0.5) * 40;
|
|
26
|
+
score += delta;
|
|
27
|
+
reasons.push(`external PR merge rate ${Math.round(signals.externalPRMergeRate * 100)}% (${delta >= 0 ? "+" : ""}${delta.toFixed(1)})`);
|
|
28
|
+
}
|
|
29
|
+
if (signals.medianFirstReviewHours !== undefined && signals.medianFirstReviewHours !== null) {
|
|
30
|
+
const penalty = -Math.min(signals.medianFirstReviewHours / 24, 14);
|
|
31
|
+
score += penalty;
|
|
32
|
+
reasons.push(`median first review ${signals.medianFirstReviewHours.toFixed(1)}h (${penalty.toFixed(1)})`);
|
|
33
|
+
}
|
|
34
|
+
if (signals.maintainerIssueResponseHours !== undefined && signals.maintainerIssueResponseHours !== null) {
|
|
35
|
+
const penalty = -Math.min(signals.maintainerIssueResponseHours / 48, 10);
|
|
36
|
+
score += penalty;
|
|
37
|
+
reasons.push(`maintainer issue response ${signals.maintainerIssueResponseHours.toFixed(1)}h (${penalty.toFixed(1)})`);
|
|
38
|
+
}
|
|
39
|
+
if (signals.openGoodFirstIssueCount !== undefined && signals.openGoodFirstIssueCount !== null) {
|
|
40
|
+
const bonus = Math.min(signals.openGoodFirstIssueCount, 20) * 0.5;
|
|
41
|
+
score += bonus;
|
|
42
|
+
if (bonus > 0)
|
|
43
|
+
reasons.push(`${signals.openGoodFirstIssueCount} open good-first-issues (+${bonus.toFixed(1)})`);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
owner: signals.owner,
|
|
47
|
+
repo: signals.repo,
|
|
48
|
+
score: Math.round(clamp(score, 0, 100)),
|
|
49
|
+
gates: signals.gates,
|
|
50
|
+
reasons,
|
|
51
|
+
signals,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=friction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"friction.js","sourceRoot":"","sources":["../src/friction.ts"],"names":[],"mappings":"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"}
|
package/dist/gates.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CONTRIBUTING.md gate parser.
|
|
3
|
+
*
|
|
4
|
+
* Problem this solves: repos have unwritten rituals before an outside PR is
|
|
5
|
+
* even allowed, and they differ per repo. FastMCP requires assignment
|
|
6
|
+
* (PrefectHQ/fastmcp#4557). trigger.dev auto-closes PRs from unvouched
|
|
7
|
+
* authors. modelcontextprotocol/servers and
|
|
8
|
+
* openai/openai-agents-python are collaborators-only for PR creation.
|
|
9
|
+
* shadcn-labs/pdfcn asks contributors to comment before starting work:
|
|
10
|
+
* that is a soft claim-first ritual, satisfied by posting a comment, not a
|
|
11
|
+
* hard assignment gate. This parser extracts those gates from contributing
|
|
12
|
+
* text so the verdict engine can flag GATED instead of letting you code
|
|
13
|
+
* first and get rejected, while soft gates like claim_first only add a note.
|
|
14
|
+
*/
|
|
15
|
+
import type { Gate, GateKind } from "./types.js";
|
|
16
|
+
export interface ParseOptions {
|
|
17
|
+
/** Repo-level policy passed in when detectable (e.g. from repo metadata). */
|
|
18
|
+
collaboratorsOnlyPolicy?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** One policy file's raw text: CONTRIBUTING variants, AGENTS.md, CLAUDE.md. */
|
|
21
|
+
export interface PolicySource {
|
|
22
|
+
file: string;
|
|
23
|
+
text: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Parse several policy files at once. Binding rules increasingly live in
|
|
27
|
+
* house-rules files that CONTRIBUTING only links to (reticle keeps its
|
|
28
|
+
* claim-first, DCO, .changes and `pnpm verify` rules in AGENTS.md), so the
|
|
29
|
+
* same gate rules run over all of them. Sources are merged without
|
|
30
|
+
* duplicating kinds: CONTRIBUTING variants win ties, and each detected gate
|
|
31
|
+
* records the file its evidence came from.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseGatesFromSources(sources: PolicySource[]): Gate[];
|
|
34
|
+
/**
|
|
35
|
+
* Parse contributing text into a gate list. Always returns all seven gates
|
|
36
|
+
* with detected true/false, plus an evidence snippet when detected.
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseGates(text: string | undefined, options?: ParseOptions): Gate[];
|
|
39
|
+
/**
|
|
40
|
+
* Parse a gatefile contribution-gates.json document into Greenlight gates.
|
|
41
|
+
* gatefile (the companion tool) extracts a repo's gates once and commits
|
|
42
|
+
* them as JSON; a repo that carries that file has already answered the gate
|
|
43
|
+
* question, so its verdicts should use the committed answer instead of
|
|
44
|
+
* re-deriving it from prose. Returns only the detected gates, each with its
|
|
45
|
+
* evidence prefixed so a verdict can say where the gate came from. Unknown
|
|
46
|
+
* kinds are skipped; malformed JSON throws.
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseGatefileJson(text: string): Gate[];
|
|
49
|
+
export declare function hardGates(gates: Gate[]): Gate[];
|
|
50
|
+
/** Human-friendly name for a gate kind. */
|
|
51
|
+
export declare function gateLabel(kind: GateKind): string;
|
package/dist/gates.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
const RULES = [
|
|
2
|
+
{
|
|
3
|
+
kind: "assignment_required",
|
|
4
|
+
regexes: [
|
|
5
|
+
/ask (to be|for) (assigned|assignment)/i,
|
|
6
|
+
/request (an )?assignment/i,
|
|
7
|
+
/must (also )?be assigned/i,
|
|
8
|
+
/assign.*before (submitting|opening|a PR)/i,
|
|
9
|
+
/please (wait|hold) (for|until).*assign/i,
|
|
10
|
+
/\bgood first issue\b.{0,40}\bassign/i,
|
|
11
|
+
],
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
// Soft ritual: comment (or claim) on the issue before starting work.
|
|
15
|
+
// Satisfied by posting a comment; never a hard block.
|
|
16
|
+
kind: "claim_first",
|
|
17
|
+
regexes: [
|
|
18
|
+
/comment.{0,60}before (starting|working|opening)/i,
|
|
19
|
+
/claim.*before (you )?start/i,
|
|
20
|
+
/comment.{0,60}before you (start|code|submit)/i,
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
kind: "vouch_required",
|
|
25
|
+
regexes: [/\bvouch/i, /vouched (author|contributor)/i],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
kind: "cla_required",
|
|
29
|
+
regexes: [
|
|
30
|
+
/contributor license agreement/i,
|
|
31
|
+
/\bcla\b.{0,20}(sign|require)/i,
|
|
32
|
+
/sign.{0,20}\bcla\b/i,
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
kind: "dco_required",
|
|
37
|
+
regexes: [
|
|
38
|
+
/\bdco\b/i,
|
|
39
|
+
/developer certificate of origin/i,
|
|
40
|
+
/sign-?off.*commit/i,
|
|
41
|
+
/signed-off-by/i,
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
kind: "discussion_first",
|
|
46
|
+
regexes: [
|
|
47
|
+
/discuss.*before (opening|submitting|a PR)/i,
|
|
48
|
+
/open an issue before/i,
|
|
49
|
+
/proposal.*(first|before)/i,
|
|
50
|
+
/rfc.*(first|before)/i,
|
|
51
|
+
/start (a |with a )?discussion/i,
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
kind: "collaborators_only",
|
|
56
|
+
regexes: [
|
|
57
|
+
/collaborators only/i,
|
|
58
|
+
/collaborators-only/i,
|
|
59
|
+
/only (repo )?collaborators/i,
|
|
60
|
+
/external (prs|pull requests) (are not|will not be) accepted/i,
|
|
61
|
+
// Maintainer-only PR shops (field cases 2026-10-15): the MCP inspector
|
|
62
|
+
// and servers repos take issues and prompts, not outside PRs, and say
|
|
63
|
+
// so in workflow language rather than gate language — "Pull requests
|
|
64
|
+
// against this repo are opened by the repo maintainers only" and
|
|
65
|
+
// "We accept issues, not pull requests". Both scored GO with "no
|
|
66
|
+
// blocking gates" until these phrasings were added. The optional
|
|
67
|
+
// asterisks tolerate the Markdown bold both repos wrap the words in.
|
|
68
|
+
/pull requests? (against|in|on) this (repo|repository) are opened by (the )?(\*\*)?(repo |repository )?maintainers(\*\*)? only/i,
|
|
69
|
+
/accept issues, not pull requests?/i,
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
/** Cut a short evidence snippet around the first regex match. */
|
|
74
|
+
function snippet(text, regex) {
|
|
75
|
+
const match = regex.exec(text);
|
|
76
|
+
if (!match)
|
|
77
|
+
return "";
|
|
78
|
+
const idx = Math.max(0, match.index - 60);
|
|
79
|
+
const raw = text.slice(idx, idx + 160).replace(/\s+/g, " ").trim();
|
|
80
|
+
return raw.length > 140 ? raw.slice(0, 140) + "..." : raw;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Parse several policy files at once. Binding rules increasingly live in
|
|
84
|
+
* house-rules files that CONTRIBUTING only links to (reticle keeps its
|
|
85
|
+
* claim-first, DCO, .changes and `pnpm verify` rules in AGENTS.md), so the
|
|
86
|
+
* same gate rules run over all of them. Sources are merged without
|
|
87
|
+
* duplicating kinds: CONTRIBUTING variants win ties, and each detected gate
|
|
88
|
+
* records the file its evidence came from.
|
|
89
|
+
*/
|
|
90
|
+
export function parseGatesFromSources(sources) {
|
|
91
|
+
const contributingFirst = (f) => (f.toLowerCase().includes("contributing") ? 0 : 1);
|
|
92
|
+
const ordered = [...sources].sort((a, b) => contributingFirst(a.file) - contributingFirst(b.file));
|
|
93
|
+
const merged = RULES.map((r) => ({
|
|
94
|
+
kind: r.kind,
|
|
95
|
+
detected: false,
|
|
96
|
+
evidence: "",
|
|
97
|
+
source: undefined,
|
|
98
|
+
}));
|
|
99
|
+
for (const src of ordered) {
|
|
100
|
+
const gates = parseGates(src.text);
|
|
101
|
+
for (const g of gates) {
|
|
102
|
+
if (!g.detected)
|
|
103
|
+
continue;
|
|
104
|
+
const m = merged.find((x) => x.kind === g.kind);
|
|
105
|
+
if (m && !m.detected) {
|
|
106
|
+
m.detected = true;
|
|
107
|
+
m.evidence = g.evidence;
|
|
108
|
+
m.source = src.file;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return merged;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Parse contributing text into a gate list. Always returns all seven gates
|
|
116
|
+
* with detected true/false, plus an evidence snippet when detected.
|
|
117
|
+
*/
|
|
118
|
+
export function parseGates(text, options = {}) {
|
|
119
|
+
const body = text ?? "";
|
|
120
|
+
return RULES.map((rule) => {
|
|
121
|
+
const found = rule.regexes.find((re) => re.test(body));
|
|
122
|
+
if (rule.kind === "collaborators_only" && options.collaboratorsOnlyPolicy) {
|
|
123
|
+
return {
|
|
124
|
+
kind: rule.kind,
|
|
125
|
+
detected: true,
|
|
126
|
+
evidence: "repo policy: PR creation restricted to collaborators",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
kind: rule.kind,
|
|
131
|
+
detected: Boolean(found),
|
|
132
|
+
evidence: found ? snippet(body, found) : "",
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* gatefile's contribution-gates.json kinds mapped onto Greenlight kinds.
|
|
138
|
+
* The two tools share a taxonomy with a few aliases: gatefile's
|
|
139
|
+
* discuss_first and issue_first are Greenlight's discussion_first, and its
|
|
140
|
+
* no_direct_prs is the hard block Greenlight calls collaborators_only.
|
|
141
|
+
*/
|
|
142
|
+
const GATEFILE_KIND_MAP = {
|
|
143
|
+
claim_first: "claim_first",
|
|
144
|
+
assignment_required: "assignment_required",
|
|
145
|
+
vouch_required: "vouch_required",
|
|
146
|
+
collaborators_only: "collaborators_only",
|
|
147
|
+
dco_required: "dco_required",
|
|
148
|
+
cla_required: "cla_required",
|
|
149
|
+
discuss_first: "discussion_first",
|
|
150
|
+
issue_first: "discussion_first",
|
|
151
|
+
no_direct_prs: "collaborators_only",
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Parse a gatefile contribution-gates.json document into Greenlight gates.
|
|
155
|
+
* gatefile (the companion tool) extracts a repo's gates once and commits
|
|
156
|
+
* them as JSON; a repo that carries that file has already answered the gate
|
|
157
|
+
* question, so its verdicts should use the committed answer instead of
|
|
158
|
+
* re-deriving it from prose. Returns only the detected gates, each with its
|
|
159
|
+
* evidence prefixed so a verdict can say where the gate came from. Unknown
|
|
160
|
+
* kinds are skipped; malformed JSON throws.
|
|
161
|
+
*/
|
|
162
|
+
export function parseGatefileJson(text) {
|
|
163
|
+
const doc = JSON.parse(text);
|
|
164
|
+
if (!doc || !Array.isArray(doc.gates)) {
|
|
165
|
+
throw new Error("not a contribution-gates.json document (no gates array)");
|
|
166
|
+
}
|
|
167
|
+
const out = [];
|
|
168
|
+
for (const g of doc.gates) {
|
|
169
|
+
const kind = g.kind ? GATEFILE_KIND_MAP[g.kind] : undefined;
|
|
170
|
+
if (!kind || out.some((x) => x.kind === kind))
|
|
171
|
+
continue;
|
|
172
|
+
out.push({
|
|
173
|
+
kind,
|
|
174
|
+
detected: true,
|
|
175
|
+
evidence: `gatefile: ${g.evidence ?? g.kind}`,
|
|
176
|
+
source: g.source ?? "contribution-gates.json",
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
/** Gates that block an outside PR before it can even be opened. */
|
|
182
|
+
const HARD_GATES = ["assignment_required", "vouch_required", "collaborators_only"];
|
|
183
|
+
export function hardGates(gates) {
|
|
184
|
+
return gates.filter((g) => g.detected && HARD_GATES.includes(g.kind));
|
|
185
|
+
}
|
|
186
|
+
/** Human-friendly name for a gate kind. */
|
|
187
|
+
export function gateLabel(kind) {
|
|
188
|
+
switch (kind) {
|
|
189
|
+
case "assignment_required": return "assignment required before PR";
|
|
190
|
+
case "claim_first": return "comment first before starting";
|
|
191
|
+
case "vouch_required": return "vouch required before PR";
|
|
192
|
+
case "cla_required": return "CLA signature required";
|
|
193
|
+
case "dco_required": return "DCO sign-off required";
|
|
194
|
+
case "discussion_first": return "discussion first";
|
|
195
|
+
case "collaborators_only": return "PR creation restricted to collaborators";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=gates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gates.js","sourceRoot":"","sources":["../src/gates.ts"],"names":[],"mappings":"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"}
|
package/dist/github.d.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { IssueSnapshot, LinkedPR, CommentSignal, GateKind, MentionedPR } from "./types.js";
|
|
2
|
+
export declare class RateLimitError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
/** Thrown by apiGet/apiPost for HTTP 404, so callers can treat "missing" as data. */
|
|
6
|
+
export declare class ApiNotFoundError extends Error {
|
|
7
|
+
constructor(path: string);
|
|
8
|
+
}
|
|
9
|
+
export declare function githubToken(): string | undefined;
|
|
10
|
+
export declare function apiGet(path: string): Promise<unknown>;
|
|
11
|
+
/** POST helper for write operations (automation commands). Read paths use apiGet. */
|
|
12
|
+
export declare function apiPost(path: string, body: unknown): Promise<unknown>;
|
|
13
|
+
export interface IssueURL {
|
|
14
|
+
owner: string;
|
|
15
|
+
repo: string;
|
|
16
|
+
number: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function parseIssueUrl(url: string): IssueURL;
|
|
19
|
+
/** Fetch issue comments for claim detection. */
|
|
20
|
+
export declare function fetchComments(owner: string, repo: string, number: number): Promise<CommentSignal[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Find PRs that reference an issue. Timeline cross-references and a text
|
|
23
|
+
* search for the issue number are discovery only: every candidate PR is
|
|
24
|
+
* then fetched individually and scored with classifyLinkStrength, so a PR
|
|
25
|
+
* that merely lists a sibling issue (e.g. pdfcn #91 closing #70 while
|
|
26
|
+
* mentioning #61) is stored as a "mention", not a "closing" PR. See issue #1.
|
|
27
|
+
*/
|
|
28
|
+
export declare function fetchLinkedPRs(owner: string, repo: string, number: number): Promise<LinkedPR[]>;
|
|
29
|
+
/** Minimal PR detail needed for the comment-mention cross-check. */
|
|
30
|
+
export interface PullRefDetail {
|
|
31
|
+
number: number;
|
|
32
|
+
state: "open" | "closed";
|
|
33
|
+
merged: boolean;
|
|
34
|
+
title: string;
|
|
35
|
+
body: string;
|
|
36
|
+
url: string;
|
|
37
|
+
}
|
|
38
|
+
export interface MentionedPRDeps {
|
|
39
|
+
fetchPull?: (owner: string, repo: string, number: number) => Promise<PullRefDetail | null>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Policy files fetched for gate extraction. Binding contribution rules
|
|
43
|
+
* increasingly live in house-rules files that CONTRIBUTING only links to
|
|
44
|
+
* (reticle keeps its claim-first, DCO, .changes and `pnpm verify` rules in
|
|
45
|
+
* AGENTS.md), so CONTRIBUTING variants are fetched together with AGENTS.md
|
|
46
|
+
* and CLAUDE.md at the root and under .github/. fastmcp moved its rules
|
|
47
|
+
* again in 2026, into docs/development/contributing.mdx behind a pointer
|
|
48
|
+
* CONTRIBUTING.md, which is why the development-guide paths are here too.
|
|
49
|
+
*/
|
|
50
|
+
export declare const HOUSE_RULE_FILES: readonly ["CONTRIBUTING.md", "contributing.md", ".github/CONTRIBUTING.md", "docs/development/contributing.mdx", "docs/development/contributing.md", "AGENTS.md", "CLAUDE.md", ".github/AGENTS.md", ".github/CLAUDE.md"];
|
|
51
|
+
export interface HouseRuleDeps {
|
|
52
|
+
/** Test seam: fetch a repo-root file's raw text, or undefined when absent. */
|
|
53
|
+
fetchRaw?: (file: string) => Promise<string | undefined>;
|
|
54
|
+
}
|
|
55
|
+
/** Fetch all existing house-rules files for a repo (best-effort, cached). */
|
|
56
|
+
export declare function fetchHouseRules(owner: string, repo: string, deps?: HouseRuleDeps): Promise<{
|
|
57
|
+
file: string;
|
|
58
|
+
text: string;
|
|
59
|
+
}[]>;
|
|
60
|
+
/** Fetch CONTRIBUTING.md raw text; returns undefined when the repo has none. */
|
|
61
|
+
export declare function fetchContributing(owner: string, repo: string): Promise<string | undefined>;
|
|
62
|
+
/**
|
|
63
|
+
* Extract PR references from comment bodies: bare #NNN mentions and full
|
|
64
|
+
* github.com/<owner>/<repo>/pull/NNN URLs. Returns deduplicated numbers in
|
|
65
|
+
* order of first appearance.
|
|
66
|
+
*/
|
|
67
|
+
export declare function extractPRReferences(comments: CommentSignal[], owner: string, repo: string): number[];
|
|
68
|
+
/**
|
|
69
|
+
* PR references per citing comment author: number -> first comment author
|
|
70
|
+
* that cited it. Used to tag MentionedPRs with fromAuthor.
|
|
71
|
+
*/
|
|
72
|
+
export declare function extractPRReferenceAuthors(comments: CommentSignal[], owner: string, repo: string): Map<number, string>;
|
|
73
|
+
/**
|
|
74
|
+
* Cross-check PR references found in comments against the PR API.
|
|
75
|
+
*
|
|
76
|
+
* Motivation: reticlehq/reticle#691 got a clean GO verdict while its comments
|
|
77
|
+
* contained a partial claim ("Taking the remaining fallback ask") and a
|
|
78
|
+
* heads-up that PR #861 covers that work. Neither signal existed in the
|
|
79
|
+
* timeline or the issue-number search, so both were missed.
|
|
80
|
+
*
|
|
81
|
+
* All comments are scanned (PR references are cheap to extract), but each
|
|
82
|
+
* result records whether the citing comment came from the issue author.
|
|
83
|
+
* The open-PR downgrade (rule 11) only trusts non-author heads-ups; the
|
|
84
|
+
* partially-addressed rule (rule 12) also reads the author's, because the
|
|
85
|
+
* maintainer narrating what already shipped is usually the issue author.
|
|
86
|
+
* References already found via the timeline/search, and the issue's own
|
|
87
|
+
* number, are skipped. Bounded at 5 PR fetches per call; the walk is
|
|
88
|
+
* best-effort and cached like the rest of the issue data.
|
|
89
|
+
*/
|
|
90
|
+
export declare function fetchCommentMentionedPRs(owner: string, repo: string, issueNumber: number, comments: CommentSignal[], knownPRNumbers: number[], deps?: MentionedPRDeps & {
|
|
91
|
+
issueAuthor?: string;
|
|
92
|
+
}): Promise<MentionedPR[]>;
|
|
93
|
+
/** Count open issues labelled good-first-issue or help-wanted. */
|
|
94
|
+
export declare function countStarterIssues(owner: string, repo: string): Promise<number | null>;
|
|
95
|
+
/** Detect a collaborators-only PR creation policy via the community metrics endpoint. */
|
|
96
|
+
export declare function detectRepoPolicy(owner: string, repo: string): Promise<{
|
|
97
|
+
collaboratorsOnly: boolean;
|
|
98
|
+
}>;
|
|
99
|
+
/** Build a full IssueSnapshot from the live API. */
|
|
100
|
+
export declare function buildIssueSnapshot(owner: string, repo: string, number: number): Promise<IssueSnapshot>;
|
|
101
|
+
export interface ScanCandidate {
|
|
102
|
+
number: number;
|
|
103
|
+
title: string;
|
|
104
|
+
url: string;
|
|
105
|
+
labels: string[];
|
|
106
|
+
updatedAt: string;
|
|
107
|
+
}
|
|
108
|
+
/** List open good-first-issue / help-wanted issues for scan mode. */
|
|
109
|
+
export declare function listStarterIssues(owner: string, repo: string, limit?: number): Promise<ScanCandidate[]>;
|
|
110
|
+
/** Raw issue-list item shape from GET /repos/:o/:r/issues. */
|
|
111
|
+
interface IssueListItem {
|
|
112
|
+
number: number;
|
|
113
|
+
title: string;
|
|
114
|
+
html_url: string;
|
|
115
|
+
labels: Array<{
|
|
116
|
+
name: string;
|
|
117
|
+
}>;
|
|
118
|
+
updated_at: string;
|
|
119
|
+
pull_request?: unknown;
|
|
120
|
+
assignees?: Array<{
|
|
121
|
+
login: string;
|
|
122
|
+
}>;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Filter raw issue-list items down to scan candidates: real issues (not
|
|
126
|
+
* pull requests) with no assignees, newest first. Pure so tests can drive
|
|
127
|
+
* it with fixtures.
|
|
128
|
+
*/
|
|
129
|
+
export declare function toScanCandidates(items: IssueListItem[]): ScanCandidate[];
|
|
130
|
+
/**
|
|
131
|
+
* Fallback candidate list for scan mode: the most recent open unassigned
|
|
132
|
+
* issues in the repo, for repos whose landable issues do not carry
|
|
133
|
+
* good-first-issue / help-wanted labels. Lists broadly (up to `cap`) so
|
|
134
|
+
* the caller can take the newest slice.
|
|
135
|
+
*/
|
|
136
|
+
export declare function listRecentUnassignedIssues(owner: string, repo: string, cap?: number): Promise<ScanCandidate[]>;
|
|
137
|
+
export interface ScanCandidateList {
|
|
138
|
+
issues: ScanCandidate[];
|
|
139
|
+
/** True when the label pass came back empty and the fallback list was used. */
|
|
140
|
+
fallbackUsed: boolean;
|
|
141
|
+
}
|
|
142
|
+
export interface ScanDeps {
|
|
143
|
+
starter?: (owner: string, repo: string, limit: number) => Promise<ScanCandidate[]>;
|
|
144
|
+
recent?: (owner: string, repo: string, cap: number) => Promise<ScanCandidate[]>;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Scan candidates for a repo: the label-tagged starter list first; when it
|
|
148
|
+
* is empty, fall back to recent open unassigned issues (opt out with
|
|
149
|
+
* fallback: false). Injectable deps keep tests offline.
|
|
150
|
+
*/
|
|
151
|
+
export declare function scanCandidates(owner: string, repo: string, opts?: {
|
|
152
|
+
fallback?: boolean;
|
|
153
|
+
limit?: number;
|
|
154
|
+
}, deps?: ScanDeps): Promise<ScanCandidateList>;
|
|
155
|
+
export type { GateKind };
|