@reneturcios/greenlight 0.5.0 → 0.7.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.
@@ -0,0 +1,254 @@
1
+ /**
2
+ * The Verified PR evidence pack. One markdown document per PR that a
3
+ * reader can check without trusting Greenlight's word: every claim in
4
+ * it cites its source (the check run, the review anchor, the claim
5
+ * comment URL).
6
+ *
7
+ * Assembled from the existing engines: the guard, CI triage, review
8
+ * triage, and the Claim Protocol state of the PR's linked issues. The
9
+ * test-suite delta is included only when the caller passes a prior
10
+ * `greenlight verify --json` report via --verify-json: the pack never
11
+ * runs suites itself (they take minutes and belong where the code is).
12
+ */
13
+ import { apiGet, parsePullUrl, parseIssueUrl } from "./github.js";
14
+ import { guardPr } from "./guard.js";
15
+ import { triagePr } from "./ci-triage.js";
16
+ import { triageReview as defaultTriageReview } from "./review-triage.js";
17
+ import { fetchProtocolState } from "./protocol-fetch.js";
18
+ import { readFileSync } from "node:fs";
19
+ import { createRequire } from "node:module";
20
+ const GREENLIGHT_VERSION = (() => {
21
+ try {
22
+ return createRequire(import.meta.url)("../package.json").version;
23
+ }
24
+ catch {
25
+ return "unknown";
26
+ }
27
+ })();
28
+ async function defaultFetchPrMeta(owner, repo, number) {
29
+ const p = (await apiGet(`/repos/${owner}/${repo}/pulls/${number}`));
30
+ return {
31
+ createdAt: String(p.created_at ?? ""),
32
+ headSha: String(p.head?.sha ?? ""),
33
+ };
34
+ }
35
+ async function defaultReadVerifyJson(path) {
36
+ try {
37
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
38
+ if (parsed && typeof parsed === "object" && typeof parsed.outcome === "string")
39
+ return parsed;
40
+ return null;
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ function cite(url) {
47
+ return url && url.length > 0 ? url : "(no link recorded)";
48
+ }
49
+ /**
50
+ * Assemble the evidence pack. Best-effort throughout: a source that
51
+ * cannot be read becomes an explicit "could not be read" line, never a
52
+ * silent gap and never invented content.
53
+ */
54
+ export async function buildEvidencePack(prUrl, opts = {}) {
55
+ const deps = opts.deps ?? {};
56
+ const now = opts.now ?? new Date();
57
+ const { owner, repo, number } = parsePullUrl(prUrl);
58
+ const guard = deps.guard ?? guardPr;
59
+ const triageCi = deps.triageCi ?? triagePr;
60
+ const triageReview = deps.triageReview ?? defaultTriageReview;
61
+ const fetchPrMeta = deps.fetchPrMeta ?? defaultFetchPrMeta;
62
+ const protocolStatus = deps.protocolStatus ?? fetchProtocolState;
63
+ const readVerifyJson = deps.readVerifyJson ?? defaultReadVerifyJson;
64
+ const [guardRes, ciRes, reviewRes, meta] = await Promise.all([
65
+ guard(prUrl).catch((e) => ({ error: e instanceof Error ? e.message : String(e) })),
66
+ triageCi(prUrl).catch((e) => ({ error: e instanceof Error ? e.message : String(e) })),
67
+ triageReview(prUrl).catch((e) => ({ error: e instanceof Error ? e.message : String(e) })),
68
+ fetchPrMeta(owner, repo, number).catch(() => ({ createdAt: "", headSha: "" })),
69
+ ]);
70
+ const guardOk = (r) => typeof r === "object" && r !== null && !("error" in r);
71
+ const ciOk = (r) => typeof r === "object" && r !== null && !("error" in r);
72
+ const reviewOk = (r) => typeof r === "object" && r !== null && !("error" in r);
73
+ // Protocol status for each linked issue the guard found.
74
+ const issueProtocols = [];
75
+ if (guardOk(guardRes)) {
76
+ for (const li of guardRes.linkedIssues) {
77
+ try {
78
+ const { owner: iOwner, repo: iRepo, number: iNumber } = parseIssueUrl(li.url);
79
+ const proto = await protocolStatus(iOwner, iRepo, iNumber).catch(() => null);
80
+ const active = proto?.computation.active ?? null;
81
+ issueProtocols.push({
82
+ issue: `${iOwner}/${iRepo}#${iNumber}`,
83
+ url: li.url,
84
+ activeClaim: active
85
+ ? {
86
+ claimant: active.claimant,
87
+ state: active.state,
88
+ claimedAt: active.claimedAt,
89
+ expiresAt: active.expiresAt,
90
+ commentUrl: active.commentUrl,
91
+ }
92
+ : null,
93
+ queue: (proto?.computation.queue ?? []).map((q) => q.claimant),
94
+ });
95
+ }
96
+ catch {
97
+ issueProtocols.push({ issue: li.url, url: li.url, activeClaim: null, queue: [] });
98
+ }
99
+ }
100
+ }
101
+ let verifySection;
102
+ let verifyJson = null;
103
+ if (opts.verifyJsonPath) {
104
+ const vr = await readVerifyJson(opts.verifyJsonPath).catch(() => null);
105
+ if (vr && vr.delta) {
106
+ verifyJson = { outcome: vr.outcome, delta: vr.delta };
107
+ const d = vr.delta;
108
+ verifySection =
109
+ `Outcome: **${vr.outcome}** (from the provided verify report; the pack does not run suites itself).\n\n` +
110
+ `- newly failing (yours): ${d.newlyFailing.length > 0 ? d.newlyFailing.join(", ") : "none"}\n` +
111
+ `- newly passing (fixed by this PR): ${d.newlyPassing.length > 0 ? d.newlyPassing.join(", ") : "none"}\n` +
112
+ `- still failing (pre-existing): ${d.stillFailing.length > 0 ? d.stillFailing.join(", ") : "none"}\n` +
113
+ `- base ${d.baseTotal.passed}/${d.baseTotal.failed} passed/failed; head ${d.headTotal.passed}/${d.headTotal.failed} passed/failed`;
114
+ }
115
+ else {
116
+ verifySection = `No usable verify report at \`${opts.verifyJsonPath}\` (unreadable or not a verify result). Run \`greenlight verify --json\` where the code lives and pass it with --verify-json.`;
117
+ }
118
+ }
119
+ else {
120
+ verifySection = "Not included. Run `greenlight verify --json` where the code lives and pass the report with --verify-json to add the honest red-green delta.";
121
+ }
122
+ const lines = [];
123
+ const title = guardOk(guardRes) ? guardRes.title : "(title unavailable)";
124
+ const author = guardOk(guardRes) ? guardRes.author : "(unknown)";
125
+ lines.push(`# Evidence pack: ${owner}/${repo}#${number}`);
126
+ lines.push("");
127
+ lines.push(`"${title}" by @${author}. PR opened ${meta.createdAt ? meta.createdAt.slice(0, 10) : "(date unavailable)"}, head ${meta.headSha ? meta.headSha.slice(0, 7) : "(unknown)"}.`);
128
+ lines.push(`Assembled ${now.toISOString().slice(0, 10)} by Greenlight v${GREENLIGHT_VERSION}. Every claim below cites its source.`);
129
+ lines.push("");
130
+ // Guard section
131
+ lines.push("## Guard verdict");
132
+ if (guardOk(guardRes)) {
133
+ lines.push(guardRes.summary);
134
+ const flags = guardRes.findings.filter((f) => f.severity === "flag");
135
+ if (flags.length === 0) {
136
+ lines.push("No flags. Source: the guard's five checks (competing fix, prior claims, artifacts, first-time, protocol claims).");
137
+ }
138
+ else {
139
+ lines.push("");
140
+ lines.push("| Finding | Evidence |");
141
+ lines.push("|---|---|");
142
+ for (const f of flags) {
143
+ lines.push(`| ${f.title} | ${f.evidence.join(" ")} |`);
144
+ }
145
+ }
146
+ const notes = guardRes.findings.filter((f) => f.severity === "note");
147
+ if (notes.length > 0) {
148
+ lines.push("");
149
+ lines.push("Notes: " + notes.map((f) => f.evidence.join(" ")).join(" "));
150
+ }
151
+ }
152
+ else {
153
+ lines.push(`Could not be read this run: ${guardRes.error}`);
154
+ }
155
+ lines.push("");
156
+ // CI section
157
+ lines.push("## CI triage");
158
+ if (ciOk(ciRes)) {
159
+ lines.push(`Verdict: **${ciRes.verdict}**.`);
160
+ if (ciRes.checks.length === 0) {
161
+ lines.push("No failing checks on the PR head. Source: the check runs on the PR head.");
162
+ }
163
+ else {
164
+ lines.push("");
165
+ lines.push("| Check | Class | Evidence | Source |");
166
+ lines.push("|---|---|---|---|");
167
+ for (const c of ciRes.checks) {
168
+ lines.push(`| \`${c.name}\` | ${c.class}${c.lean ? ` (lean: ${c.lean})` : ""} | ${c.evidence} | ${cite(c.detailsUrl)} |`);
169
+ }
170
+ }
171
+ }
172
+ else {
173
+ lines.push(`Could not be read this run: ${ciRes.error}`);
174
+ }
175
+ lines.push("");
176
+ // Review section
177
+ lines.push("## Review triage");
178
+ if (reviewOk(reviewRes)) {
179
+ const c = reviewRes.counts;
180
+ lines.push(`Verdict: **${reviewRes.verdict}** (${c.likelyReal} likely-real, ${c.needsDecision} needs-decision, ${c.likelyNoise} likely-noise).`);
181
+ const actionable = reviewRes.findings.filter((f) => f.class === "likely-real" || f.class === "needs-decision");
182
+ if (actionable.length === 0) {
183
+ lines.push("No actionable findings. Source: review and issue comments on the PR.");
184
+ }
185
+ else {
186
+ lines.push("");
187
+ lines.push("| Finding | Class | Reviewer | Source |");
188
+ lines.push("|---|---|---|---|");
189
+ for (const f of actionable) {
190
+ const anchor = f.path ? `${f.path}${f.line ? `:${f.line}` : ""}` : "(review-level)";
191
+ lines.push(`| ${f.summary} | ${f.class} | @${f.author} (${f.authorKind}) | ${anchor} |`);
192
+ }
193
+ lines.push("");
194
+ lines.push("Reply drafts for each finding are in the review triage output; this pack records the findings, not the replies.");
195
+ }
196
+ }
197
+ else {
198
+ lines.push(`Could not be read this run: ${reviewRes.error}`);
199
+ }
200
+ lines.push("");
201
+ // Protocol section
202
+ lines.push("## Claim Protocol status of linked issues");
203
+ if (issueProtocols.length === 0) {
204
+ lines.push("No linked issues with readable protocol state.");
205
+ }
206
+ else {
207
+ for (const ip of issueProtocols) {
208
+ if (!ip.activeClaim) {
209
+ lines.push(`- ${ip.issue}: no protocol claims. Source: ${cite(ip.url)}`);
210
+ }
211
+ else {
212
+ const a = ip.activeClaim;
213
+ lines.push(`- ${ip.issue}: **${a.state}** claim held by @${a.claimant}, claimed ${a.claimedAt.slice(0, 10)}, expires ${a.expiresAt.slice(0, 10)}. Source: ${cite(a.commentUrl)}`);
214
+ if (ip.queue.length > 0)
215
+ lines.push(` Queue: ${ip.queue.map((q) => `@${q}`).join(", ")}`);
216
+ }
217
+ }
218
+ }
219
+ lines.push("");
220
+ // Verify section
221
+ lines.push("## Test-suite delta");
222
+ lines.push(verifySection);
223
+ lines.push("");
224
+ // Sources
225
+ lines.push("## Sources");
226
+ lines.push(`- PR: ${cite(prUrl)}`);
227
+ if (guardOk(guardRes)) {
228
+ for (const li of guardRes.linkedIssues)
229
+ lines.push(`- Linked issue: ${cite(li.url)}`);
230
+ }
231
+ lines.push(`- Pack generator: Greenlight v${GREENLIGHT_VERSION} (guard + ci_triage + review_triage + protocol engines).`);
232
+ lines.push("");
233
+ const markdown = lines.join("\n");
234
+ return {
235
+ pr: `${owner}/${repo}#${number}`,
236
+ title,
237
+ author,
238
+ assembledAt: now.toISOString(),
239
+ version: GREENLIGHT_VERSION,
240
+ markdown,
241
+ json: {
242
+ pr: `${owner}/${repo}#${number}`,
243
+ title,
244
+ author,
245
+ version: GREENLIGHT_VERSION,
246
+ guard: guardOk(guardRes) ? { summary: guardRes.summary, flags: guardRes.findings.filter((f) => f.severity === "flag").map((f) => ({ check: f.check, title: f.title, evidence: f.evidence })) } : { error: guardRes.error },
247
+ ci: ciOk(ciRes) ? { verdict: ciRes.verdict, checks: ciRes.checks } : { error: ciRes.error },
248
+ review: reviewOk(reviewRes) ? { verdict: reviewRes.verdict, counts: reviewRes.counts, findings: reviewRes.findings } : { error: reviewRes.error },
249
+ protocol: issueProtocols,
250
+ verify: verifyJson,
251
+ },
252
+ };
253
+ }
254
+ //# sourceMappingURL=evidence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evidence.js","sourceRoot":"","sources":["../src/evidence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,OAAO,EAAoB,MAAM,YAAY,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAuB,MAAM,gBAAgB,CAAC;AAC/D,OAAO,EAAE,YAAY,IAAI,mBAAmB,EAA2B,MAAM,oBAAoB,CAAC;AAClG,OAAO,EAAE,kBAAkB,EAA4B,MAAM,qBAAqB,CAAC;AAEnF,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,kBAAkB,GAAW,CAAC,GAAG,EAAE;IACvC,IAAI,CAAC;QACH,OAAQ,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,iBAAiB,CAAyB,CAAC,OAAO,CAAC;IAC5F,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC,CAAC,EAAE,CAAC;AAgBL,KAAK,UAAU,kBAAkB,CAAC,KAAa,EAAE,IAAY,EAAE,MAAc;IAC3E,MAAM,CAAC,GAAG,CAAC,MAAM,MAAM,CAAC,UAAU,KAAK,IAAI,IAAI,UAAU,MAAM,EAAE,CAAC,CAAgB,CAAC;IACnF,OAAO;QACL,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU,IAAI,EAAE,CAAC;QACrC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,IAAI,EAAE,CAAC;KACnC,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,qBAAqB,CAAC,IAAY;IAC/C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAiB,CAAC;QACtE,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ;YAAE,OAAO,MAAM,CAAC;QAC9F,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAyBD,SAAS,IAAI,CAAC,GAA8B;IAC1C,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,oBAAoB,CAAC;AAC5D,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,KAAa,EAAE,OAAwB,EAAE;IAC/E,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC;IAC7B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC;IACnC,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IAEpD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,OAAO,CAAC;IACpC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC;IAC3C,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,mBAAmB,CAAC;IAC9D,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,kBAAkB,CAAC;IAC3D,MAAM,cAAc,GAAG,IAAI,CAAC,cAAc,IAAI,kBAAkB,CAAC;IACjE,MAAM,cAAc,GAAG,IAAI,CAAC,cAAc,IAAI,qBAAqB,CAAC;IAEpE,MAAM,CAAC,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC3D,KAAK,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAClF,QAAQ,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACrF,YAAY,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACzF,WAAW,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;KAC/E,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,CAAC,CAAU,EAAoB,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC;IACzG,MAAM,IAAI,GAAG,CAAC,CAAU,EAAuB,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC;IACzG,MAAM,QAAQ,GAAG,CAAC,CAAU,EAA2B,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC;IAEjH,yDAAyD;IACzD,MAAM,cAAc,GAA4B,EAAE,CAAC;IACnD,IAAI,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACtB,KAAK,MAAM,EAAE,IAAI,QAAQ,CAAC,YAAY,EAAE,CAAC;YACvC,IAAI,CAAC;gBACH,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,aAAa,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;gBAC9E,MAAM,KAAK,GAAG,MAAM,cAAc,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;gBAC7E,MAAM,MAAM,GAAG,KAAK,EAAE,WAAW,CAAC,MAAM,IAAI,IAAI,CAAC;gBACjD,cAAc,CAAC,IAAI,CAAC;oBAClB,KAAK,EAAE,GAAG,MAAM,IAAI,KAAK,IAAI,OAAO,EAAE;oBACtC,GAAG,EAAE,EAAE,CAAC,GAAG;oBACX,WAAW,EAAE,MAAM;wBACjB,CAAC,CAAC;4BACE,QAAQ,EAAE,MAAM,CAAC,QAAQ;4BACzB,KAAK,EAAE,MAAM,CAAC,KAAK;4BACnB,SAAS,EAAE,MAAM,CAAC,SAAS;4BAC3B,SAAS,EAAE,MAAM,CAAC,SAAS;4BAC3B,UAAU,EAAE,MAAM,CAAC,UAAU;yBAC9B;wBACH,CAAC,CAAC,IAAI;oBACR,KAAK,EAAE,CAAC,KAAK,EAAE,WAAW,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC;iBAC/D,CAAC,CAAC;YACL,CAAC;YAAC,MAAM,CAAC;gBACP,cAAc,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;YACpF,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,aAAqB,CAAC;IAC1B,IAAI,UAAU,GAAmC,IAAI,CAAC;IACtD,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;QACxB,MAAM,EAAE,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QACvE,IAAI,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC;YACnB,UAAU,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC;YACtD,MAAM,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC;YACnB,aAAa;gBACX,cAAc,EAAE,CAAC,OAAO,gFAAgF;oBACxG,4BAA4B,CAAC,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,IAAI;oBAC9F,uCAAuC,CAAC,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,IAAI;oBACzG,mCAAmC,CAAC,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,IAAI;oBACrG,UAAU,CAAC,CAAC,SAAS,CAAC,MAAM,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM,wBAAwB,CAAC,CAAC,SAAS,CAAC,MAAM,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM,gBAAgB,CAAC;QACvI,CAAC;aAAM,CAAC;YACN,aAAa,GAAG,gCAAgC,IAAI,CAAC,cAAc,+HAA+H,CAAC;QACrM,CAAC;IACH,CAAC;SAAM,CAAC;QACN,aAAa,GAAG,6IAA6I,CAAC;IAChK,CAAC;IAED,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,qBAAqB,CAAC;IACzE,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC;IACjE,KAAK,CAAC,IAAI,CAAC,oBAAoB,KAAK,IAAI,IAAI,IAAI,MAAM,EAAE,CAAC,CAAC;IAC1D,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,MAAM,eAAe,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,oBAAoB,UAAU,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC;IACzL,KAAK,CAAC,IAAI,CAAC,aAAa,GAAG,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,mBAAmB,kBAAkB,uCAAuC,CAAC,CAAC;IACpI,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,gBAAgB;IAChB,KAAK,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;IAC/B,IAAI,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACtB,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAC7B,MAAM,KAAK,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC;QACrE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,KAAK,CAAC,IAAI,CAAC,kHAAkH,CAAC,CAAC;QACjI,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACf,KAAK,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;YACrC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YACxB,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;gBACtB,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACzD,CAAC;QACH,CAAC;QACD,MAAM,KAAK,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC;QACrE,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACf,KAAK,CAAC,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,IAAI,CAAC,+BAAgC,QAA8B,CAAC,KAAK,EAAE,CAAC,CAAC;IACrF,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,aAAa;IACb,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC3B,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAChB,KAAK,CAAC,IAAI,CAAC,cAAc,KAAK,CAAC,OAAO,KAAK,CAAC,CAAC;QAC7C,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC9B,KAAK,CAAC,IAAI,CAAC,0EAA0E,CAAC,CAAC;QACzF,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACf,KAAK,CAAC,IAAI,CAAC,uCAAuC,CAAC,CAAC;YACpD,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;YAChC,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;gBAC7B,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,QAAQ,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,QAAQ,MAAM,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YAC5H,CAAC;QACH,CAAC;IACH,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,IAAI,CAAC,+BAAgC,KAA2B,CAAC,KAAK,EAAE,CAAC,CAAC;IAClF,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,iBAAiB;IACjB,KAAK,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;IAC/B,IAAI,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QACxB,MAAM,CAAC,GAAG,SAAS,CAAC,MAAM,CAAC;QAC3B,KAAK,CAAC,IAAI,CAAC,cAAc,SAAS,CAAC,OAAO,OAAO,CAAC,CAAC,UAAU,iBAAiB,CAAC,CAAC,aAAa,oBAAoB,CAAC,CAAC,WAAW,iBAAiB,CAAC,CAAC;QACjJ,MAAM,UAAU,GAAG,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,aAAa,IAAI,CAAC,CAAC,KAAK,KAAK,gBAAgB,CAAC,CAAC;QAC/G,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5B,KAAK,CAAC,IAAI,CAAC,sEAAsE,CAAC,CAAC;QACrF,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACf,KAAK,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAC;YACtD,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;YAChC,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;gBAC3B,MAAM,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,gBAAgB,CAAC;gBACpF,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,MAAM,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,UAAU,OAAO,MAAM,IAAI,CAAC,CAAC;YAC3F,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACf,KAAK,CAAC,IAAI,CAAC,iHAAiH,CAAC,CAAC;QAChI,CAAC;IACH,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,IAAI,CAAC,+BAAgC,SAA+B,CAAC,KAAK,EAAE,CAAC,CAAC;IACtF,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,mBAAmB;IACnB,KAAK,CAAC,IAAI,CAAC,2CAA2C,CAAC,CAAC;IACxD,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChC,KAAK,CAAC,IAAI,CAAC,gDAAgD,CAAC,CAAC;IAC/D,CAAC;SAAM,CAAC;QACN,KAAK,MAAM,EAAE,IAAI,cAAc,EAAE,CAAC;YAChC,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC;gBACpB,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,iCAAiC,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC3E,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC;gBACzB,KAAK,CAAC,IAAI,CACR,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,KAAK,qBAAqB,CAAC,CAAC,QAAQ,aAAa,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,EAAE,CACtK,CAAC;gBACF,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;oBAAE,KAAK,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAC7F,CAAC;QACH,CAAC;IACH,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,iBAAiB;IACjB,KAAK,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;IAClC,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC1B,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,UAAU;IACV,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IACzB,KAAK,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACnC,IAAI,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACtB,KAAK,MAAM,EAAE,IAAI,QAAQ,CAAC,YAAY;YAAE,KAAK,CAAC,IAAI,CAAC,mBAAmB,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACxF,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,iCAAiC,kBAAkB,0DAA0D,CAAC,CAAC;IAC1H,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClC,OAAO;QACL,EAAE,EAAE,GAAG,KAAK,IAAI,IAAI,IAAI,MAAM,EAAE;QAChC,KAAK;QACL,MAAM;QACN,WAAW,EAAE,GAAG,CAAC,WAAW,EAAE;QAC9B,OAAO,EAAE,kBAAkB;QAC3B,QAAQ;QACR,IAAI,EAAE;YACJ,EAAE,EAAE,GAAG,KAAK,IAAI,IAAI,IAAI,MAAM,EAAE;YAChC,KAAK;YACL,MAAM;YACN,OAAO,EAAE,kBAAkB;YAC3B,KAAK,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAG,QAA8B,CAAC,KAAK,EAAE;YACjP,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAG,KAA2B,CAAC,KAAK,EAAE;YAClH,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,QAAQ,EAAE,SAAS,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAG,SAA+B,CAAC,KAAK,EAAE;YACxK,QAAQ,EAAE,cAAc;YACxB,MAAM,EAAE,UAAU;SACnB;KACF,CAAC;AACJ,CAAC"}
package/dist/guard.d.ts CHANGED
@@ -1,14 +1,32 @@
1
1
  import type { CommentSignal, LinkedPR } from "./types.js";
2
2
  import { type ProtocolFetchResult } from "./protocol-fetch.js";
3
- export declare const GUARD_VERSION = "0.5.0";
3
+ export declare const GUARD_VERSION = "0.6.0";
4
4
  /** The hidden marker the GitHub Action uses to find its own comment and update it. */
5
5
  export declare const GUARD_COMMENT_MARKER = "<!-- greenlight-guard -->";
6
6
  /** Check ids, also the keys of .greenlight-guard.json. */
7
7
  export declare const GUARD_CHECKS: readonly ["competing-fix", "other-claim", "missing-artifact", "first-time", "protocol-claim"];
8
8
  export type GuardCheckId = (typeof GUARD_CHECKS)[number];
9
- export type GuardConfig = Record<GuardCheckId, boolean>;
9
+ export interface AutopilotConfig {
10
+ /**
11
+ * Machine check id (e.g. "competing-fix", "protocol-claim",
12
+ * "changeset-missing") -> label name to apply. Only findings with
13
+ * severity "flag" can earn a label; notes never do.
14
+ */
15
+ labels: Record<string, string>;
16
+ /**
17
+ * When true, the guard comment includes ready-to-copy stale-claim nudge
18
+ * lines for the maintainer. The guard never posts them anywhere itself.
19
+ */
20
+ nudge: boolean;
21
+ }
22
+ export interface GuardConfig extends Record<GuardCheckId, boolean> {
23
+ /** Autopilot is off when absent: the default. Maintainer opts in. */
24
+ autopilot?: AutopilotConfig | null;
25
+ }
10
26
  export declare function defaultGuardConfig(): GuardConfig;
11
- /** Read .greenlight-guard.json from a directory (default cwd). Best-effort: anything unreadable means all checks on. */
27
+ /** Parse the autopilot section of a raw config object. Anything malformed means off. */
28
+ export declare function parseAutopilotConfig(raw: unknown): AutopilotConfig | null;
29
+ /** Read .greenlight-guard.json from a directory (default cwd). Best-effort: anything unreadable means all checks on and autopilot off. */
12
30
  export declare function readGuardConfig(dir?: string): GuardConfig;
13
31
  export interface GuardLinkedIssue {
14
32
  number: number;
@@ -61,6 +79,21 @@ export interface GuardResult {
61
79
  summary: string;
62
80
  /** True when at least one finding has severity "flag". */
63
81
  hasFindings: boolean;
82
+ /**
83
+ * Autopilot label decisions: label names to apply, computed from the
84
+ * config's label map over severity-"flag" findings only. Empty unless
85
+ * the maintainer enabled autopilot in .greenlight-guard.json. The guard
86
+ * never applies these itself; the bundled Action does.
87
+ */
88
+ autopilotLabels: string[];
89
+ /** The check-by-check decisions behind autopilotLabels (for dry-run display). */
90
+ autopilotLabelDecisions: AutopilotLabelDecision[];
91
+ /**
92
+ * Ready-to-copy stale-claim nudge lines for the maintainer, included in
93
+ * the guard comment when autopilot.nudge is on. The guard never posts
94
+ * them anywhere itself.
95
+ */
96
+ staleClaimNudges: string[];
64
97
  /** The comment-ready markdown. */
65
98
  markdown: string;
66
99
  }
@@ -154,6 +187,28 @@ export declare function checkMissingArtifacts(args: {
154
187
  prAuthor: string;
155
188
  prComments: CommentSignal[];
156
189
  }): GuardFinding[];
190
+ /** A claim is stale when it is 14+ days old and no open PR has appeared on its issue. */
191
+ export declare const STALE_CLAIM_NUDGE_DAYS = 14;
192
+ export interface AutopilotLabelDecision {
193
+ check: string;
194
+ title: string;
195
+ label: string;
196
+ }
197
+ /**
198
+ * Decide autopilot labels: the config's label map over severity-"flag"
199
+ * findings only. Pure, so fixtures can prove the mapping and the default-off.
200
+ * Notes never earn labels.
201
+ */
202
+ export declare function decideAutopilotLabels(findings: GuardFinding[], autopilot: AutopilotConfig | null | undefined): AutopilotLabelDecision[];
203
+ /**
204
+ * Build ready-to-copy nudge lines for stale foreign claims: 14+ days old
205
+ * with no OTHER open PR on the same issue (the guarded PR is the new
206
+ * arrival; another open PR means the claim is not stale). The maintainer
207
+ * copies and posts them on the issue; the guard never posts on the issue
208
+ * itself (sovereignty). Pure over the guard's own claims and competing
209
+ * PRs.
210
+ */
211
+ export declare function buildStaleClaimNudges(claims: GuardClaim[], competingPRs: GuardCompetingPR[], guardedPrNumber: number): string[];
157
212
  /** Light author-history signal. Null means the lookup failed: no finding. */
158
213
  export declare function checkFirstTime(author: string, priorMerged: number | null): GuardFinding | null;
159
214
  export declare function renderGuardMarkdown(result: Omit<GuardResult, "markdown">): string;
@@ -175,3 +230,8 @@ export interface GuardDeps {
175
230
  * be read degrades to "no evidence" and its check is skipped.
176
231
  */
177
232
  export declare function guardPr(prUrl: string, deps?: GuardDeps, config?: GuardConfig): Promise<GuardResult>;
233
+ /**
234
+ * The `--labels-dry-run` rendering: what autopilot WOULD do, in plain
235
+ * text, without touching anything. Never posts, never labels.
236
+ */
237
+ export declare function renderLabelsDryRun(result: GuardResult): string;
package/dist/guard.js CHANGED
@@ -44,6 +44,22 @@
44
44
  * "other-claim", "missing-artifact", "first-time", "protocol-claim");
45
45
  * unknown keys are ignored, a missing or unreadable file means every
46
46
  * check is on.
47
+ *
48
+ * Autopilot (check 6, maintainer-side action, off by default): the same
49
+ * config file may carry an "autopilot" section with a label map and a
50
+ * nudge toggle:
51
+ *
52
+ * { "autopilot": { "labels": { "protocol-claim": "claim-conflict",
53
+ * "competing-fix": "duplicate-effort",
54
+ * "changeset-missing": "needs-changeset" },
55
+ * "nudge": true } }
56
+ *
57
+ * When enabled, the guard COMPUTES label decisions (check id -> label
58
+ * name, flags only) and ready-to-copy stale-claim nudge lines. It never
59
+ * applies labels or posts comments itself: the bundled GitHub Action
60
+ * reads the decisions from the guard JSON and applies the labels with
61
+ * the Actions token. The CLI reports the decisions with
62
+ * --labels-dry-run.
47
63
  */
48
64
  import { readFileSync } from "node:fs";
49
65
  import { apiGet, parsePullUrl, fetchLinkedPRs, fetchComments } from "./github.js";
@@ -53,15 +69,35 @@ import { detectClaPath, detectChangesetConvention } from "./prep-detail.js";
53
69
  import { titleSlug } from "./automate.js";
54
70
  import { parseGates } from "./gates.js";
55
71
  import { fetchProtocolState } from "./protocol-fetch.js";
56
- export const GUARD_VERSION = "0.5.0";
72
+ export const GUARD_VERSION = "0.6.0";
57
73
  /** The hidden marker the GitHub Action uses to find its own comment and update it. */
58
74
  export const GUARD_COMMENT_MARKER = "<!-- greenlight-guard -->";
59
75
  /** Check ids, also the keys of .greenlight-guard.json. */
60
76
  export const GUARD_CHECKS = ["competing-fix", "other-claim", "missing-artifact", "first-time", "protocol-claim"];
61
77
  export function defaultGuardConfig() {
62
- return { "competing-fix": true, "other-claim": true, "missing-artifact": true, "first-time": true, "protocol-claim": true };
78
+ return { "competing-fix": true, "other-claim": true, "missing-artifact": true, "first-time": true, "protocol-claim": true, autopilot: null };
79
+ }
80
+ /** Parse the autopilot section of a raw config object. Anything malformed means off. */
81
+ export function parseAutopilotConfig(raw) {
82
+ if (!raw || typeof raw !== "object")
83
+ return null;
84
+ const obj = raw;
85
+ const ap = obj["autopilot"];
86
+ if (!ap || typeof ap !== "object")
87
+ return null;
88
+ const apObj = ap;
89
+ const labels = {};
90
+ const rawLabels = apObj["labels"];
91
+ if (rawLabels && typeof rawLabels === "object") {
92
+ for (const [k, v] of Object.entries(rawLabels)) {
93
+ if (typeof k === "string" && k.trim() && typeof v === "string" && v.trim()) {
94
+ labels[k.trim()] = v.trim();
95
+ }
96
+ }
97
+ }
98
+ return { labels, nudge: apObj["nudge"] === true };
63
99
  }
64
- /** Read .greenlight-guard.json from a directory (default cwd). Best-effort: anything unreadable means all checks on. */
100
+ /** Read .greenlight-guard.json from a directory (default cwd). Best-effort: anything unreadable means all checks on and autopilot off. */
65
101
  export function readGuardConfig(dir = process.cwd()) {
66
102
  const config = defaultGuardConfig();
67
103
  try {
@@ -70,9 +106,10 @@ export function readGuardConfig(dir = process.cwd()) {
70
106
  if (typeof raw[id] === "boolean")
71
107
  config[id] = raw[id];
72
108
  }
109
+ config.autopilot = parseAutopilotConfig(raw);
73
110
  }
74
111
  catch {
75
- // Missing or broken config: all checks stay on.
112
+ // Missing or broken config: all checks stay on, autopilot stays off.
76
113
  }
77
114
  return config;
78
115
  }
@@ -279,6 +316,57 @@ export function checkMissingArtifacts(args) {
279
316
  }
280
317
  return findings;
281
318
  }
319
+ /** A claim is stale when it is 14+ days old and no open PR has appeared on its issue. */
320
+ export const STALE_CLAIM_NUDGE_DAYS = 14;
321
+ /**
322
+ * Decide autopilot labels: the config's label map over severity-"flag"
323
+ * findings only. Pure, so fixtures can prove the mapping and the default-off.
324
+ * Notes never earn labels.
325
+ */
326
+ export function decideAutopilotLabels(findings, autopilot) {
327
+ if (!autopilot)
328
+ return [];
329
+ const seen = new Set();
330
+ const decisions = [];
331
+ for (const f of findings) {
332
+ if (f.severity !== "flag")
333
+ continue;
334
+ const label = autopilot.labels[f.check];
335
+ if (typeof label !== "string" || label.trim().length === 0)
336
+ continue;
337
+ const key = label.trim();
338
+ if (seen.has(key))
339
+ continue;
340
+ seen.add(key);
341
+ decisions.push({ check: f.check, title: f.title, label: key });
342
+ }
343
+ return decisions;
344
+ }
345
+ /**
346
+ * Build ready-to-copy nudge lines for stale foreign claims: 14+ days old
347
+ * with no OTHER open PR on the same issue (the guarded PR is the new
348
+ * arrival; another open PR means the claim is not stale). The maintainer
349
+ * copies and posts them on the issue; the guard never posts on the issue
350
+ * itself (sovereignty). Pure over the guard's own claims and competing
351
+ * PRs.
352
+ */
353
+ export function buildStaleClaimNudges(claims, competingPRs, guardedPrNumber) {
354
+ const nudges = [];
355
+ for (const c of claims) {
356
+ if (c.claimedByAuthor)
357
+ continue;
358
+ if (c.ageDays < STALE_CLAIM_NUDGE_DAYS)
359
+ continue;
360
+ const ref = c.issueRef ?? undefined;
361
+ const hasCompeting = competingPRs.some((p) => p.issueNumber === c.issueNumber && (p.issueRef ?? undefined) === ref);
362
+ if (hasCompeting)
363
+ continue;
364
+ const label = c.issueRef ?? `#${c.issueNumber}`;
365
+ nudges.push(`@${c.author} claimed ${label} ${c.ageDays} days ago and never opened a PR. ` +
366
+ `This PR (#${guardedPrNumber}) now addresses it — if you're still working on it, reply to keep your claim; otherwise the claim lapses.`);
367
+ }
368
+ return nudges;
369
+ }
282
370
  /** Light author-history signal. Null means the lookup failed: no finding. */
283
371
  export function checkFirstTime(author, priorMerged) {
284
372
  if (priorMerged === null)
@@ -326,6 +414,13 @@ export function renderGuardMarkdown(result) {
326
414
  }
327
415
  lines.push("");
328
416
  }
417
+ if (result.staleClaimNudges.length > 0) {
418
+ lines.push("For the maintainer (autopilot nudge lines — copy and paste onto the issue; the guard never posts these itself):");
419
+ for (const n of result.staleClaimNudges) {
420
+ lines.push(`> ${n}`);
421
+ }
422
+ lines.push("");
423
+ }
329
424
  lines.push("---");
330
425
  lines.push(`_Checked with [Greenlight guard](https://github.com/CloudCorpRecords/greenlight) v${GUARD_VERSION}; findings are signals, verify before acting._`);
331
426
  return lines.join("\n");
@@ -644,6 +739,12 @@ export async function guardPr(prUrl, deps = {}, config = defaultGuardConfig()) {
644
739
  findings.push(f);
645
740
  }
646
741
  const { summary, hasFindings } = buildGuardSummary(findings);
742
+ // Autopilot: computed, never applied, here. Labels come from the
743
+ // maintainer's label map over severity-"flag" findings; nudge lines are
744
+ // ready-to-copy text the maintainer posts on the issue themselves.
745
+ const autopilot = config.autopilot ?? null;
746
+ const labelDecisions = decideAutopilotLabels(findings, autopilot);
747
+ const staleClaimNudges = autopilot?.nudge ? buildStaleClaimNudges(claims, competingPRs, number) : [];
647
748
  const result = {
648
749
  pr: prUrl,
649
750
  number,
@@ -656,9 +757,35 @@ export async function guardPr(prUrl, deps = {}, config = defaultGuardConfig()) {
656
757
  findings,
657
758
  summary,
658
759
  hasFindings,
760
+ autopilotLabels: labelDecisions.map((d) => d.label),
761
+ autopilotLabelDecisions: labelDecisions,
762
+ staleClaimNudges,
659
763
  markdown: "",
660
764
  };
661
765
  result.markdown = renderGuardMarkdown(result);
662
766
  return result;
663
767
  }
768
+ /**
769
+ * The `--labels-dry-run` rendering: what autopilot WOULD do, in plain
770
+ * text, without touching anything. Never posts, never labels.
771
+ */
772
+ export function renderLabelsDryRun(result) {
773
+ const lines = [`Autopilot label dry run for ${result.repo}#${result.number}`];
774
+ const decisions = result.autopilotLabelDecisions;
775
+ if (decisions.length === 0) {
776
+ lines.push("No autopilot labels: the maintainer has not enabled autopilot, or no mapped flag fired.");
777
+ }
778
+ else {
779
+ lines.push(`Would apply ${decisions.length} label${decisions.length === 1 ? "" : "s"}: ${decisions.map((d) => d.label).join(", ")}`);
780
+ for (const d of decisions) {
781
+ lines.push(` "${d.check}" (${d.title}) -> "${d.label}"`);
782
+ }
783
+ }
784
+ if (result.staleClaimNudges.length > 0) {
785
+ lines.push("Stale-claim nudge lines (for the maintainer to post on the issue):");
786
+ for (const n of result.staleClaimNudges)
787
+ lines.push(` > ${n}`);
788
+ }
789
+ return lines.join("\n");
790
+ }
664
791
  //# sourceMappingURL=guard.js.map