@reneturcios/greenlight 0.18.0 → 0.19.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/README.md +14 -3
- package/dist/cli.js +73 -5
- package/dist/cli.js.map +1 -1
- package/dist/friction.d.ts +16 -1
- package/dist/friction.js +55 -15
- package/dist/friction.js.map +1 -1
- package/dist/github.d.ts +113 -2
- package/dist/github.js +211 -42
- package/dist/github.js.map +1 -1
- package/dist/help.js +7 -5
- package/dist/help.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +41 -0
- package/dist/verdict.d.ts +16 -1
- package/dist/verdict.js +36 -2
- package/dist/verdict.js.map +1 -1
- package/package.json +1 -1
- package/server.json +3 -3
package/dist/friction.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { gateLabel, parseGatesFromSources } from "./gates.js";
|
|
2
|
-
import {
|
|
2
|
+
import { fetchPolicySourcesWithProvenance, detectRepoPolicy, countStarterIssues, countStarterCandidates, resolveCanonicalRepo, } from "./github.js";
|
|
3
3
|
import { fetchMergeVelocity } from "./merge-velocity.js";
|
|
4
4
|
import { detectBehavioralGate } from "./behavior.js";
|
|
5
5
|
const GATE_WEIGHTS = {
|
|
@@ -106,9 +106,22 @@ export function attachOutsiderShare(report) {
|
|
|
106
106
|
* and docs/, plus the org-level AI_POLICY.md), starter-issue count, and
|
|
107
107
|
* merge velocity as a best-effort signal. Shared by the CLI, the MCP
|
|
108
108
|
* repo_friction tool, and the protocol preflight (which pairs it with
|
|
109
|
-
* the issue status).
|
|
109
|
+
* the issue status). Every report carries policy_provenance: whether
|
|
110
|
+
* each policy source was read, absent, or a fetch error.
|
|
111
|
+
*
|
|
112
|
+
* v0.19.0 lite mode (opts.lite, friction --lite) is the canonical
|
|
113
|
+
* lightweight computation: policy gates ONLY. It runs the policy-source
|
|
114
|
+
* fetch plus parseGatesFromSources (and the collaboratorsOnly push from
|
|
115
|
+
* detectRepoPolicy, which is free), and scores with scoreFriction using
|
|
116
|
+
* all outcome signals null and behavioralGate null, so the score is
|
|
117
|
+
* 100 + gate weights, clamped, with the same GATE_WEIGHTS. It skips the
|
|
118
|
+
* behavioral pass, merge velocity, and the starter-issue count, marks
|
|
119
|
+
* the report lite: true, and sets signals.note to "lite". Repo rename
|
|
120
|
+
* resolution stays: it is one cheap call and name correctness matters
|
|
121
|
+
* for the scanner.
|
|
110
122
|
*/
|
|
111
123
|
export async function fetchFrictionSummary(owner, repo, opts = {}) {
|
|
124
|
+
const lite = opts.lite === true;
|
|
112
125
|
// v0.18.0 (item 15): renamed repos (dbt-labs/dbt-core -> dbt-labs/dbt)
|
|
113
126
|
// 422 search and confuse every name-based flow; resolve once, use the
|
|
114
127
|
// canonical name everywhere below, and carry the old name in
|
|
@@ -117,16 +130,24 @@ export async function fetchFrictionSummary(owner, repo, opts = {}) {
|
|
|
117
130
|
const resolvedFrom = canon.renamed ? `${owner}/${repo}` : undefined;
|
|
118
131
|
owner = canon.owner;
|
|
119
132
|
repo = canon.repo;
|
|
120
|
-
const [
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
133
|
+
const [sourcesResult, policy, starterCount, mergeVelocity] = lite
|
|
134
|
+
? await Promise.all([
|
|
135
|
+
fetchPolicySourcesWithProvenance(owner, repo),
|
|
136
|
+
detectRepoPolicy(owner, repo),
|
|
137
|
+
Promise.resolve(null),
|
|
138
|
+
Promise.resolve(null),
|
|
139
|
+
])
|
|
140
|
+
: await Promise.all([
|
|
141
|
+
fetchPolicySourcesWithProvenance(owner, repo),
|
|
142
|
+
detectRepoPolicy(owner, repo),
|
|
143
|
+
countStarterIssues(owner, repo),
|
|
144
|
+
// v0.18.0 (item 11): merge velocity feeds the documented external
|
|
145
|
+
// merge-rate score term (see the formula in the module header).
|
|
146
|
+
// Best-effort: a failed fetch leaves the term null and unscored,
|
|
147
|
+
// never fabricated.
|
|
148
|
+
fetchMergeVelocity(owner, repo).catch(() => null),
|
|
149
|
+
]);
|
|
150
|
+
const sources = sourcesResult.sources;
|
|
130
151
|
const gates = parseGatesFromSources(sources);
|
|
131
152
|
if (policy.collaboratorsOnly && !gates.some((g) => g.kind === "collaborators_only" && g.detected)) {
|
|
132
153
|
gates.push({
|
|
@@ -137,9 +158,9 @@ export async function fetchFrictionSummary(owner, repo, opts = {}) {
|
|
|
137
158
|
}
|
|
138
159
|
// Behavioral gate is a signal, best-effort and silent on failure: a
|
|
139
160
|
// dead pass must never fabricate a hostile verdict, and a missing one
|
|
140
|
-
// must never hide an actual gate.
|
|
161
|
+
// must never hide an actual gate. Lite mode skips it entirely.
|
|
141
162
|
let behavioralGate = null;
|
|
142
|
-
if (opts.behavioral !== false) {
|
|
163
|
+
if (!lite && opts.behavioral !== false) {
|
|
143
164
|
try {
|
|
144
165
|
behavioralGate = await detectBehavioralGate(owner, repo);
|
|
145
166
|
}
|
|
@@ -147,6 +168,19 @@ export async function fetchFrictionSummary(owner, repo, opts = {}) {
|
|
|
147
168
|
behavioralGate = null;
|
|
148
169
|
}
|
|
149
170
|
}
|
|
171
|
+
// v0.19.0 (item 16): when the classic starter labels read exactly 0,
|
|
172
|
+
// one bounded fallback estimates open unassigned candidates under a
|
|
173
|
+
// wider label set. The estimate is reported next to the label count,
|
|
174
|
+
// never merged into it and never scored.
|
|
175
|
+
let starterCandidateEstimate = null;
|
|
176
|
+
if (starterCount === 0) {
|
|
177
|
+
try {
|
|
178
|
+
starterCandidateEstimate = await countStarterCandidates(owner, repo);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
starterCandidateEstimate = null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
150
184
|
const signals = {
|
|
151
185
|
owner,
|
|
152
186
|
repo,
|
|
@@ -156,11 +190,17 @@ export async function fetchFrictionSummary(owner, repo, opts = {}) {
|
|
|
156
190
|
medianFirstReviewHours: null,
|
|
157
191
|
maintainerIssueResponseHours: null,
|
|
158
192
|
openGoodFirstIssueCount: starterCount,
|
|
193
|
+
starterLabelCount: starterCount,
|
|
194
|
+
starterCandidateEstimate,
|
|
159
195
|
behavioralGate,
|
|
160
|
-
note: "live",
|
|
196
|
+
note: lite ? "lite" : "live",
|
|
161
197
|
};
|
|
162
198
|
const report = attachBehavioralGate(scoreFriction(signals), behavioralGate);
|
|
163
199
|
report.mergeVelocity = mergeVelocity;
|
|
200
|
+
// v0.19.0 (item 7): every report says how its policy sources were obtained.
|
|
201
|
+
report.policy_provenance = sourcesResult.provenance;
|
|
202
|
+
if (lite)
|
|
203
|
+
report.lite = true;
|
|
164
204
|
if (resolvedFrom)
|
|
165
205
|
report.resolvedFrom = resolvedFrom;
|
|
166
206
|
return attachOutsiderShare(report);
|
package/dist/friction.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"friction.js","sourceRoot":"","sources":["../src/friction.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"friction.js","sourceRoot":"","sources":["../src/friction.ts"],"names":[],"mappings":"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"}
|
package/dist/github.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IssueSnapshot, LinkedPR, CommentSignal, GateKind, Gate, MentionedPR, DuplicateTarget } from "./types.js";
|
|
1
|
+
import type { IssueSnapshot, LinkedPR, CommentSignal, GateKind, Gate, MentionedPR, DuplicateTarget, PolicySourceProvenance } from "./types.js";
|
|
2
2
|
import { type WorkflowFile } from "./gates.js";
|
|
3
3
|
/**
|
|
4
4
|
* The GitHub API base in use. Reads GREENLIGHT_GITHUB_BASE_URL so a
|
|
@@ -105,11 +105,31 @@ export interface MentionedPRDeps {
|
|
|
105
105
|
*/
|
|
106
106
|
export declare const HOUSE_RULE_FILES: readonly ["CONTRIBUTING.md", ".github/CONTRIBUTING.md", "docs/CONTRIBUTING.md", "contributing.md", "docs/development/contributing.mdx", "docs/development/contributing.md", "AGENTS.md", ".github/AGENTS.md", "docs/AGENTS.md", "CLAUDE.md", ".github/CLAUDE.md", "docs/CLAUDE.md", "AI_POLICY.md", ".github/AI_POLICY.md", "docs/AI_POLICY.md"];
|
|
107
107
|
export interface HouseRuleDeps {
|
|
108
|
-
/**
|
|
108
|
+
/**
|
|
109
|
+
* Test seam: fetch a repo-root file's raw text, or undefined when absent.
|
|
110
|
+
* v0.19.0: a fetchRaw that throws surfaces as a provenance "error" (with
|
|
111
|
+
* the error as the kind string), never as "absent".
|
|
112
|
+
*/
|
|
109
113
|
fetchRaw?: (file: string) => Promise<string | undefined>;
|
|
110
114
|
/** Test seam: fetch the org's AI_POLICY.md text, or undefined when absent. */
|
|
111
115
|
fetchOrgPolicy?: (owner: string) => Promise<string | undefined>;
|
|
112
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* v0.19.0: the detailed outcome of one raw policy-file fetch. The old
|
|
119
|
+
* fetchRawFile collapsed "no such file" and "the fetch failed" into the
|
|
120
|
+
* same undefined, so friction could not tell a repo with no policy from
|
|
121
|
+
* a repo whose policy read broke. "error" carries a short kind string
|
|
122
|
+
* (e.g. "rate limited", "http 500", "network error").
|
|
123
|
+
*/
|
|
124
|
+
export type RawFileFetchStatus = {
|
|
125
|
+
status: "read";
|
|
126
|
+
text: string;
|
|
127
|
+
} | {
|
|
128
|
+
status: "absent";
|
|
129
|
+
} | {
|
|
130
|
+
status: "error";
|
|
131
|
+
error: string;
|
|
132
|
+
};
|
|
113
133
|
/**
|
|
114
134
|
* v0.9.3: org-level AI policy. Binding agent rules sometimes live in the
|
|
115
135
|
* org's `.github` repo rather than the project repo (astral-sh's
|
|
@@ -117,17 +137,77 @@ export interface HouseRuleDeps {
|
|
|
117
137
|
* CONTRIBUTING only links to mastodon/.github's AI_POLICY.md). Probed
|
|
118
138
|
* once per org and cached under its own key so a sweep over N repos from
|
|
119
139
|
* one org costs one fetch, not N.
|
|
140
|
+
*
|
|
141
|
+
* v0.19.0: the cache entry now carries the full detailed outcome
|
|
142
|
+
* (read/absent/error); entries written before v0.19.0 (a bare text or
|
|
143
|
+
* empty string) are treated as a miss and re-fetched.
|
|
120
144
|
*/
|
|
145
|
+
export declare function fetchOrgAiPolicyDetailed(owner: string, deps?: HouseRuleDeps): Promise<{
|
|
146
|
+
file: string;
|
|
147
|
+
outcome: RawFileFetchStatus;
|
|
148
|
+
}>;
|
|
149
|
+
/** Org-level AI policy text, or undefined when absent or unreadable. */
|
|
121
150
|
export declare function fetchOrgAiPolicy(owner: string, deps?: HouseRuleDeps): Promise<string | undefined>;
|
|
122
151
|
/**
|
|
123
152
|
* v0.9.3: the full policy source set for gate detection: house-rules
|
|
124
153
|
* files (first hit per filename across root, .github/, docs/) plus the
|
|
125
154
|
* org-level AI_POLICY.md when the org publishes one.
|
|
155
|
+
*
|
|
156
|
+
* v0.19.0: fetchPolicySourcesWithProvenance adds the read provenance
|
|
157
|
+
* friction reports; this plain variant delegates and returns the sources
|
|
158
|
+
* alone, so its callers do not change.
|
|
126
159
|
*/
|
|
160
|
+
export declare function fetchPolicySourcesWithProvenance(owner: string, repo: string, deps?: HouseRuleDeps): Promise<{
|
|
161
|
+
sources: {
|
|
162
|
+
file: string;
|
|
163
|
+
text: string;
|
|
164
|
+
}[];
|
|
165
|
+
provenance: PolicySourceProvenance[];
|
|
166
|
+
}>;
|
|
127
167
|
export declare function fetchPolicySources(owner: string, repo: string, deps?: HouseRuleDeps): Promise<{
|
|
128
168
|
file: string;
|
|
129
169
|
text: string;
|
|
130
170
|
}[]>;
|
|
171
|
+
/**
|
|
172
|
+
* v0.9.3: fetch one policy file's raw text, honoring the API base
|
|
173
|
+
* override. In proxy mode (GREENLIGHT_GITHUB_BASE_URL set) the raw host
|
|
174
|
+
* is unreachable with the surrogate credential, so policy files are read
|
|
175
|
+
* through the contents API instead; otherwise the raw host is used as
|
|
176
|
+
* before.
|
|
177
|
+
*
|
|
178
|
+
* v0.19.0: fetchRawFileDetailed distinguishes three outcomes: read,
|
|
179
|
+
* absent (the file genuinely is not there), and error (the fetch
|
|
180
|
+
* failed). fetchRawFile stays as a thin wrapper that returns the text
|
|
181
|
+
* only when the fetch read, so its callers do not change.
|
|
182
|
+
*/
|
|
183
|
+
export declare function fetchRawFileDetailed(owner: string, repo: string, file: string, headers: Record<string, string>): Promise<RawFileFetchStatus>;
|
|
184
|
+
/**
|
|
185
|
+
* Fetch all existing house-rules files for a repo (best-effort, cached),
|
|
186
|
+
* with per-basename read provenance.
|
|
187
|
+
*
|
|
188
|
+
* v0.9.3: stop at the first hit per filename (root, then .github/, then
|
|
189
|
+
* docs/), so a repo with CONTRIBUTING.md at the root does not pay for
|
|
190
|
+
* two more 404s, and a repo with it only in docs/ is still found (the
|
|
191
|
+
* AFFiNE shape the 5k scan caught: root-only probing called it
|
|
192
|
+
* "no policy" while docs/CONTRIBUTING.md existed).
|
|
193
|
+
*
|
|
194
|
+
* v0.19.0: provenance carries ONE entry per basename probed: the path
|
|
195
|
+
* that was read (status "read"), the first candidate path with status
|
|
196
|
+
* "absent" when every location missed, or status "error" with the first
|
|
197
|
+
* error kind when a fetch failed before any hit. An error at one
|
|
198
|
+
* location does not stop the later locations: the read still wins when
|
|
199
|
+
* a later location has the file.
|
|
200
|
+
*
|
|
201
|
+
* The cache payload is { sources, provenance }; entries written before
|
|
202
|
+
* v0.19.0 (a bare sources array) count as a miss and are re-fetched.
|
|
203
|
+
*/
|
|
204
|
+
export declare function fetchHouseRulesDetailed(owner: string, repo: string, deps?: HouseRuleDeps): Promise<{
|
|
205
|
+
sources: {
|
|
206
|
+
file: string;
|
|
207
|
+
text: string;
|
|
208
|
+
}[];
|
|
209
|
+
provenance: PolicySourceProvenance[];
|
|
210
|
+
}>;
|
|
131
211
|
/** Fetch all existing house-rules files for a repo (best-effort, cached). */
|
|
132
212
|
export declare function fetchHouseRules(owner: string, repo: string, deps?: HouseRuleDeps): Promise<{
|
|
133
213
|
file: string;
|
|
@@ -245,6 +325,23 @@ export declare function extractTimelineCrossRefPRs(events: Record<string, unknow
|
|
|
245
325
|
}>;
|
|
246
326
|
/** Count open issues labelled good-first-issue or help-wanted. */
|
|
247
327
|
export declare function countStarterIssues(owner: string, repo: string): Promise<number | null>;
|
|
328
|
+
/**
|
|
329
|
+
* v0.19.0 (item 16): the wider starter label set for the candidate
|
|
330
|
+
* estimate. Repos that never adopted the classic "good first issue" /
|
|
331
|
+
* "help wanted" pair often label starter work one of these instead.
|
|
332
|
+
*/
|
|
333
|
+
export declare const STARTER_CANDIDATE_LABELS: readonly ["beginner friendly", "beginner", "easy", "starter", "first-timers-only", "up-for-grabs", "good-first-issue"];
|
|
334
|
+
export interface StarterSearchDeps {
|
|
335
|
+
/** Test seam: run a search query, return its total_count or null. */
|
|
336
|
+
searchCount?: (query: string) => Promise<number | null>;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Fallback starter-candidate estimate, used only when the classic
|
|
340
|
+
* label count is exactly 0. One search call: open, unassigned issues
|
|
341
|
+
* under the wider starter label set. An estimate for display, never a
|
|
342
|
+
* score input; null when the search itself fails.
|
|
343
|
+
*/
|
|
344
|
+
export declare function countStarterCandidates(owner: string, repo: string, deps?: StarterSearchDeps): Promise<number | null>;
|
|
248
345
|
/** Detect a collaborators-only PR creation policy via the community metrics endpoint. */
|
|
249
346
|
export declare function detectRepoPolicy(owner: string, repo: string): Promise<{
|
|
250
347
|
collaboratorsOnly: boolean;
|
|
@@ -292,6 +389,20 @@ export interface ScanCandidateList {
|
|
|
292
389
|
/** True when the label pass came back empty and the fallback list was used. */
|
|
293
390
|
fallbackUsed: boolean;
|
|
294
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* Default wall-time bound for `greenlight scan`, in seconds (v0.19.0,
|
|
394
|
+
* item 17): on mega-backlog repos the per-issue verdict pipeline ran
|
|
395
|
+
* about 25 minutes, so the scan stops with marked partial results when
|
|
396
|
+
* the budget runs out instead of running open-ended. Override per run
|
|
397
|
+
* with --max-time <seconds>.
|
|
398
|
+
*/
|
|
399
|
+
export declare const DEFAULT_SCAN_MAX_TIME_SEC = 600;
|
|
400
|
+
/**
|
|
401
|
+
* Pure wall-time check for the scan loop (v0.19.0, item 17): true once
|
|
402
|
+
* the elapsed time since startedAtMs has reached the budget. Call with
|
|
403
|
+
* Date.now() for the nowMs argument. Exported for unit tests.
|
|
404
|
+
*/
|
|
405
|
+
export declare function scanBudgetExceeded(startedAtMs: number, maxTimeSec: number, nowMs: number): boolean;
|
|
295
406
|
export interface ScanDeps {
|
|
296
407
|
starter?: (owner: string, repo: string, limit: number) => Promise<ScanCandidate[]>;
|
|
297
408
|
recent?: (owner: string, repo: string, cap: number) => Promise<ScanCandidate[]>;
|
package/dist/github.js
CHANGED
|
@@ -400,6 +400,17 @@ export const HOUSE_RULE_FILES = [
|
|
|
400
400
|
".github/AI_POLICY.md",
|
|
401
401
|
"docs/AI_POLICY.md",
|
|
402
402
|
];
|
|
403
|
+
/** Short kind string for a policy-file fetch failure (never the full stack). */
|
|
404
|
+
function classifyFetchError(err) {
|
|
405
|
+
if (err instanceof RateLimitError)
|
|
406
|
+
return "rate limited";
|
|
407
|
+
if (err instanceof RequestTimeoutError)
|
|
408
|
+
return "timeout";
|
|
409
|
+
if (err instanceof Error && err.message) {
|
|
410
|
+
return err.message.length > 120 ? err.message.slice(0, 120) : err.message;
|
|
411
|
+
}
|
|
412
|
+
return "fetch error";
|
|
413
|
+
}
|
|
403
414
|
/**
|
|
404
415
|
* v0.9.3: org-level AI policy. Binding agent rules sometimes live in the
|
|
405
416
|
* org's `.github` repo rather than the project repo (astral-sh's
|
|
@@ -407,95 +418,203 @@ export const HOUSE_RULE_FILES = [
|
|
|
407
418
|
* CONTRIBUTING only links to mastodon/.github's AI_POLICY.md). Probed
|
|
408
419
|
* once per org and cached under its own key so a sweep over N repos from
|
|
409
420
|
* one org costs one fetch, not N.
|
|
421
|
+
*
|
|
422
|
+
* v0.19.0: the cache entry now carries the full detailed outcome
|
|
423
|
+
* (read/absent/error); entries written before v0.19.0 (a bare text or
|
|
424
|
+
* empty string) are treated as a miss and re-fetched.
|
|
410
425
|
*/
|
|
411
|
-
export async function
|
|
426
|
+
export async function fetchOrgAiPolicyDetailed(owner, deps = {}) {
|
|
427
|
+
const file = `AI_POLICY.md (org ${owner})`;
|
|
412
428
|
if (!deps.fetchOrgPolicy) {
|
|
413
429
|
const cached = cacheGet("org-ai-policy", [owner.toLowerCase()]);
|
|
414
|
-
if (cached !== undefined)
|
|
415
|
-
return cached
|
|
430
|
+
if (cached !== undefined && typeof cached === "object")
|
|
431
|
+
return { file, outcome: cached };
|
|
416
432
|
}
|
|
417
433
|
const headers = { "User-Agent": "greenlight-cli" };
|
|
418
434
|
const token = githubToken();
|
|
419
435
|
if (token)
|
|
420
436
|
headers["Authorization"] = `Bearer ${token}`;
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
437
|
+
let outcome;
|
|
438
|
+
if (deps.fetchOrgPolicy) {
|
|
439
|
+
try {
|
|
440
|
+
const text = await deps.fetchOrgPolicy(owner);
|
|
441
|
+
outcome = text ? { status: "read", text } : { status: "absent" };
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
outcome =
|
|
445
|
+
err instanceof ApiNotFoundError
|
|
446
|
+
? { status: "absent" }
|
|
447
|
+
: { status: "error", error: classifyFetchError(err) };
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
outcome = await fetchRawFileDetailed(owner, ".github", "AI_POLICY.md", headers);
|
|
452
|
+
}
|
|
424
453
|
if (!deps.fetchOrgPolicy)
|
|
425
|
-
cacheSet("org-ai-policy", [owner.toLowerCase()],
|
|
426
|
-
return
|
|
454
|
+
cacheSet("org-ai-policy", [owner.toLowerCase()], outcome);
|
|
455
|
+
return { file, outcome };
|
|
456
|
+
}
|
|
457
|
+
/** Org-level AI policy text, or undefined when absent or unreadable. */
|
|
458
|
+
export async function fetchOrgAiPolicy(owner, deps = {}) {
|
|
459
|
+
const { outcome } = await fetchOrgAiPolicyDetailed(owner, deps);
|
|
460
|
+
return outcome.status === "read" ? outcome.text : undefined;
|
|
427
461
|
}
|
|
428
462
|
/**
|
|
429
463
|
* v0.9.3: the full policy source set for gate detection: house-rules
|
|
430
464
|
* files (first hit per filename across root, .github/, docs/) plus the
|
|
431
465
|
* org-level AI_POLICY.md when the org publishes one.
|
|
466
|
+
*
|
|
467
|
+
* v0.19.0: fetchPolicySourcesWithProvenance adds the read provenance
|
|
468
|
+
* friction reports; this plain variant delegates and returns the sources
|
|
469
|
+
* alone, so its callers do not change.
|
|
432
470
|
*/
|
|
471
|
+
export async function fetchPolicySourcesWithProvenance(owner, repo, deps = {}) {
|
|
472
|
+
const [{ sources, provenance }, { file, outcome }] = await Promise.all([
|
|
473
|
+
fetchHouseRulesDetailed(owner, repo, deps),
|
|
474
|
+
fetchOrgAiPolicyDetailed(owner, deps),
|
|
475
|
+
]);
|
|
476
|
+
if (outcome.status === "read")
|
|
477
|
+
sources.push({ file, text: outcome.text });
|
|
478
|
+
provenance.push(outcome.status === "read"
|
|
479
|
+
? { file, scope: "org", status: "read" }
|
|
480
|
+
: outcome.status === "error"
|
|
481
|
+
? { file, scope: "org", status: "error", error: outcome.error }
|
|
482
|
+
: { file, scope: "org", status: "absent" });
|
|
483
|
+
return { sources, provenance };
|
|
484
|
+
}
|
|
433
485
|
export async function fetchPolicySources(owner, repo, deps = {}) {
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
if (orgPolicy)
|
|
437
|
-
rules.push({ file: `AI_POLICY.md (org ${owner})`, text: orgPolicy });
|
|
438
|
-
return rules;
|
|
486
|
+
const { sources } = await fetchPolicySourcesWithProvenance(owner, repo, deps);
|
|
487
|
+
return sources;
|
|
439
488
|
}
|
|
440
489
|
/**
|
|
441
490
|
* v0.9.3: fetch one policy file's raw text, honoring the API base
|
|
442
491
|
* override. In proxy mode (GREENLIGHT_GITHUB_BASE_URL set) the raw host
|
|
443
492
|
* is unreachable with the surrogate credential, so policy files are read
|
|
444
493
|
* through the contents API instead; otherwise the raw host is used as
|
|
445
|
-
* before.
|
|
494
|
+
* before.
|
|
495
|
+
*
|
|
496
|
+
* v0.19.0: fetchRawFileDetailed distinguishes three outcomes: read,
|
|
497
|
+
* absent (the file genuinely is not there), and error (the fetch
|
|
498
|
+
* failed). fetchRawFile stays as a thin wrapper that returns the text
|
|
499
|
+
* only when the fetch read, so its callers do not change.
|
|
446
500
|
*/
|
|
447
|
-
async function
|
|
501
|
+
export async function fetchRawFileDetailed(owner, repo, file, headers) {
|
|
448
502
|
if (githubApiBase() !== DEFAULT_API) {
|
|
449
503
|
try {
|
|
450
504
|
const data = (await apiGet(`/repos/${owner}/${repo}/contents/${file}`));
|
|
451
505
|
if (typeof data.content === "string" && data.encoding === "base64") {
|
|
452
|
-
return Buffer.from(data.content, "base64").toString("utf8");
|
|
506
|
+
return { status: "read", text: Buffer.from(data.content, "base64").toString("utf8") };
|
|
453
507
|
}
|
|
508
|
+
return { status: "absent" };
|
|
509
|
+
}
|
|
510
|
+
catch (err) {
|
|
511
|
+
if (err instanceof ApiNotFoundError)
|
|
512
|
+
return { status: "absent" };
|
|
513
|
+
return { status: "error", error: classifyFetchError(err) };
|
|
454
514
|
}
|
|
455
|
-
catch { /* absent */ }
|
|
456
|
-
return undefined;
|
|
457
515
|
}
|
|
458
516
|
try {
|
|
459
|
-
const res = await
|
|
517
|
+
const res = await activeFetch()(`${RAW}/${owner}/${repo}/HEAD/${file}`, { headers });
|
|
460
518
|
if (res.ok)
|
|
461
|
-
return await res.text();
|
|
519
|
+
return { status: "read", text: await res.text() };
|
|
520
|
+
if (res.status === 404)
|
|
521
|
+
return { status: "absent" };
|
|
522
|
+
return { status: "error", error: `http ${res.status}` };
|
|
523
|
+
}
|
|
524
|
+
catch (err) {
|
|
525
|
+
return { status: "error", error: classifyFetchError(err) };
|
|
462
526
|
}
|
|
463
|
-
catch { /* try next */ }
|
|
464
|
-
return undefined;
|
|
465
527
|
}
|
|
466
|
-
/**
|
|
467
|
-
|
|
528
|
+
/** Returns the file's text when the fetch reads it, otherwise undefined. */
|
|
529
|
+
async function fetchRawFile(owner, repo, file, headers) {
|
|
530
|
+
const outcome = await fetchRawFileDetailed(owner, repo, file, headers);
|
|
531
|
+
return outcome.status === "read" ? outcome.text : undefined;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Fetch all existing house-rules files for a repo (best-effort, cached),
|
|
535
|
+
* with per-basename read provenance.
|
|
536
|
+
*
|
|
537
|
+
* v0.9.3: stop at the first hit per filename (root, then .github/, then
|
|
538
|
+
* docs/), so a repo with CONTRIBUTING.md at the root does not pay for
|
|
539
|
+
* two more 404s, and a repo with it only in docs/ is still found (the
|
|
540
|
+
* AFFiNE shape the 5k scan caught: root-only probing called it
|
|
541
|
+
* "no policy" while docs/CONTRIBUTING.md existed).
|
|
542
|
+
*
|
|
543
|
+
* v0.19.0: provenance carries ONE entry per basename probed: the path
|
|
544
|
+
* that was read (status "read"), the first candidate path with status
|
|
545
|
+
* "absent" when every location missed, or status "error" with the first
|
|
546
|
+
* error kind when a fetch failed before any hit. An error at one
|
|
547
|
+
* location does not stop the later locations: the read still wins when
|
|
548
|
+
* a later location has the file.
|
|
549
|
+
*
|
|
550
|
+
* The cache payload is { sources, provenance }; entries written before
|
|
551
|
+
* v0.19.0 (a bare sources array) count as a miss and are re-fetched.
|
|
552
|
+
*/
|
|
553
|
+
export async function fetchHouseRulesDetailed(owner, repo, deps = {}) {
|
|
468
554
|
if (!deps.fetchRaw) {
|
|
469
555
|
const cached = cacheGet("house-rules", [owner, repo]);
|
|
470
|
-
if (cached)
|
|
471
|
-
return cached;
|
|
556
|
+
if (cached && !Array.isArray(cached) && Array.isArray(cached.provenance)) {
|
|
557
|
+
return { sources: cached.sources, provenance: cached.provenance };
|
|
558
|
+
}
|
|
472
559
|
}
|
|
473
560
|
const headers = { "User-Agent": "greenlight-cli" };
|
|
474
561
|
const token = githubToken();
|
|
475
562
|
if (token)
|
|
476
563
|
headers["Authorization"] = `Bearer ${token}`;
|
|
477
|
-
const
|
|
478
|
-
(
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
564
|
+
const fetchOne = async (file) => {
|
|
565
|
+
if (deps.fetchRaw) {
|
|
566
|
+
try {
|
|
567
|
+
const text = await deps.fetchRaw(file);
|
|
568
|
+
return text ? { status: "read", text } : { status: "absent" };
|
|
569
|
+
}
|
|
570
|
+
catch (err) {
|
|
571
|
+
if (err instanceof ApiNotFoundError)
|
|
572
|
+
return { status: "absent" };
|
|
573
|
+
return { status: "error", error: classifyFetchError(err) };
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return fetchRawFileDetailed(owner, repo, file, headers);
|
|
577
|
+
};
|
|
578
|
+
const sources = [];
|
|
579
|
+
const provenance = [];
|
|
486
580
|
for (const file of HOUSE_RULE_FILES) {
|
|
487
581
|
const base = file.split("/").pop().toLowerCase();
|
|
488
|
-
if (
|
|
582
|
+
if (provenance.some((p) => p.file.split("/").pop().toLowerCase() === base && p.scope === "repo")) {
|
|
489
583
|
continue;
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
584
|
+
}
|
|
585
|
+
let readAt;
|
|
586
|
+
let firstError;
|
|
587
|
+
for (const candidate of HOUSE_RULE_FILES) {
|
|
588
|
+
if (candidate.split("/").pop().toLowerCase() !== base)
|
|
589
|
+
continue;
|
|
590
|
+
const outcome = await fetchOne(candidate);
|
|
591
|
+
if (outcome.status === "read") {
|
|
592
|
+
readAt = { file: candidate, text: outcome.text };
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
if (outcome.status === "error" && !firstError) {
|
|
596
|
+
firstError = { file: candidate, error: outcome.error };
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (readAt) {
|
|
600
|
+
sources.push(readAt);
|
|
601
|
+
provenance.push({ file: readAt.file, scope: "repo", status: "read" });
|
|
602
|
+
}
|
|
603
|
+
else if (firstError) {
|
|
604
|
+
provenance.push({ file: firstError.file, scope: "repo", status: "error", error: firstError.error });
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
provenance.push({ file, scope: "repo", status: "absent" });
|
|
494
608
|
}
|
|
495
609
|
}
|
|
496
610
|
if (!deps.fetchRaw)
|
|
497
|
-
cacheSet("house-rules", [owner, repo],
|
|
498
|
-
return
|
|
611
|
+
cacheSet("house-rules", [owner, repo], { sources, provenance });
|
|
612
|
+
return { sources, provenance };
|
|
613
|
+
}
|
|
614
|
+
/** Fetch all existing house-rules files for a repo (best-effort, cached). */
|
|
615
|
+
export async function fetchHouseRules(owner, repo, deps = {}) {
|
|
616
|
+
const { sources } = await fetchHouseRulesDetailed(owner, repo, deps);
|
|
617
|
+
return sources;
|
|
499
618
|
}
|
|
500
619
|
/** Fetch CONTRIBUTING.md raw text; returns undefined when the repo has none. */
|
|
501
620
|
export async function fetchContributing(owner, repo) {
|
|
@@ -888,6 +1007,40 @@ export async function countStarterIssues(owner, repo) {
|
|
|
888
1007
|
return null;
|
|
889
1008
|
}
|
|
890
1009
|
}
|
|
1010
|
+
/**
|
|
1011
|
+
* v0.19.0 (item 16): the wider starter label set for the candidate
|
|
1012
|
+
* estimate. Repos that never adopted the classic "good first issue" /
|
|
1013
|
+
* "help wanted" pair often label starter work one of these instead.
|
|
1014
|
+
*/
|
|
1015
|
+
export const STARTER_CANDIDATE_LABELS = [
|
|
1016
|
+
"beginner friendly",
|
|
1017
|
+
"beginner",
|
|
1018
|
+
"easy",
|
|
1019
|
+
"starter",
|
|
1020
|
+
"first-timers-only",
|
|
1021
|
+
"up-for-grabs",
|
|
1022
|
+
"good-first-issue",
|
|
1023
|
+
];
|
|
1024
|
+
/**
|
|
1025
|
+
* Fallback starter-candidate estimate, used only when the classic
|
|
1026
|
+
* label count is exactly 0. One search call: open, unassigned issues
|
|
1027
|
+
* under the wider starter label set. An estimate for display, never a
|
|
1028
|
+
* score input; null when the search itself fails.
|
|
1029
|
+
*/
|
|
1030
|
+
export async function countStarterCandidates(owner, repo, deps = {}) {
|
|
1031
|
+
const labels = STARTER_CANDIDATE_LABELS.map((l) => `"${l}"`).join(",");
|
|
1032
|
+
const query = `repo:${owner}/${repo} type:issue state:open no:assignee label:${labels}`;
|
|
1033
|
+
if (deps.searchCount)
|
|
1034
|
+
return deps.searchCount(query);
|
|
1035
|
+
try {
|
|
1036
|
+
const q = encodeURIComponent(query);
|
|
1037
|
+
const data = (await apiGet(`/search/issues?q=${q}&per_page=1`));
|
|
1038
|
+
return data.total_count ?? null;
|
|
1039
|
+
}
|
|
1040
|
+
catch {
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
891
1044
|
/** Detect a collaborators-only PR creation policy via the community metrics endpoint. */
|
|
892
1045
|
export async function detectRepoPolicy(owner, repo) {
|
|
893
1046
|
// Best-effort: no public API exposes pull_request_creation_policy directly,
|
|
@@ -1042,6 +1195,22 @@ export async function listRecentUnassignedIssues(owner, repo, cap = 25) {
|
|
|
1042
1195
|
const items = await paginate(`/repos/${owner}/${repo}/issues?state=open&sort=created&direction=desc`, 100, 3);
|
|
1043
1196
|
return toScanCandidates(items).slice(0, cap);
|
|
1044
1197
|
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Default wall-time bound for `greenlight scan`, in seconds (v0.19.0,
|
|
1200
|
+
* item 17): on mega-backlog repos the per-issue verdict pipeline ran
|
|
1201
|
+
* about 25 minutes, so the scan stops with marked partial results when
|
|
1202
|
+
* the budget runs out instead of running open-ended. Override per run
|
|
1203
|
+
* with --max-time <seconds>.
|
|
1204
|
+
*/
|
|
1205
|
+
export const DEFAULT_SCAN_MAX_TIME_SEC = 600;
|
|
1206
|
+
/**
|
|
1207
|
+
* Pure wall-time check for the scan loop (v0.19.0, item 17): true once
|
|
1208
|
+
* the elapsed time since startedAtMs has reached the budget. Call with
|
|
1209
|
+
* Date.now() for the nowMs argument. Exported for unit tests.
|
|
1210
|
+
*/
|
|
1211
|
+
export function scanBudgetExceeded(startedAtMs, maxTimeSec, nowMs) {
|
|
1212
|
+
return nowMs - startedAtMs >= maxTimeSec * 1000;
|
|
1213
|
+
}
|
|
1045
1214
|
/**
|
|
1046
1215
|
* Scan candidates for a repo: the label-tagged starter list first; when it
|
|
1047
1216
|
* is empty, fall back to recent open unassigned issues (opt out with
|