limn-review 0.1.138 → 0.1.140
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/result-schemas.js +3 -0
- package/dist/api/plugins/rate-limit.js +5 -0
- package/dist/api/routes/prs.js +3 -2
- package/dist/db/limn-finding-severity.js +126 -0
- package/dist/db/migrations/0134_team_claims.sql +10 -0
- package/dist/db/migrations/0135_review_pairs.sql +12 -0
- package/dist/db/migrations/0136_review_kind.sql +5 -0
- package/dist/db/migrations/meta/_journal.json +21 -0
- package/dist/db/pr-liveness.js +11 -2
- package/dist/db/queries.js +68 -6
- package/dist/db/review-threads-for-review.js +5 -2
- package/dist/db/schema.sqlite.js +23 -0
- package/dist/github/branch-queries.js +15 -1
- package/dist/github/check-context-state.js +42 -0
- package/dist/github/commit-checks.js +13 -2
- package/dist/github/mutations.js +32 -1
- package/dist/github/queries.js +6 -0
- package/dist/review/agentic.js +2 -0
- package/dist/review/claude-review/auto-post.js +109 -39
- package/dist/review/claude-review/auto-settings.js +35 -1
- package/dist/review/claude-review/auto.js +104 -11
- package/dist/review/claude-review/finding-thread.js +5 -1
- package/dist/review/claude-review/manager.js +341 -53
- package/dist/review/claude-review/pair.js +110 -0
- package/dist/review/claude-review/persist.js +149 -23
- package/dist/review/claude-review/prompts.js +42 -4
- package/dist/review/claude-review/replies-check-agent.js +154 -0
- package/dist/review/claude-review/replies-check-prompt.js +82 -0
- package/dist/review/claude-review/replies-check-routes.js +76 -0
- package/dist/review/claude-review/replies-check-schema.js +30 -0
- package/dist/review/claude-review/replies-check.js +638 -0
- package/dist/review/claude-review/replies-outcome.js +322 -0
- package/dist/review/claude-review/replies-pass.js +267 -0
- package/dist/review/claude-review/routes.js +195 -7
- package/dist/review/claude-review/team-claim.js +465 -0
- package/dist/review/post-review.js +14 -3
- package/dist/review/post-seam.js +12 -8
- package/dist/shared/ai-provider-words.js +3 -1
- package/dist/shared/index.js +2 -0
- package/dist/shared/limn-severity.js +52 -0
- package/dist/shared/limn-status.js +70 -0
- package/dist/sync/backfill-ci-history.js +32 -2
- package/dist/sync/branch-status.js +42 -4
- package/dist/sync/limn-ci.js +59 -0
- package/dist/sync/unsettled-prs.js +2 -0
- package/dist/sync/upsert.js +47 -45
- package/package.json +1 -1
- package/public/assets/{AiFixTab-DwAzsycp.js → AiFixTab-D8HrsNZV.js} +3 -3
- package/public/assets/ClaudeReviewTab-CJpvYk_E.js +20 -0
- package/public/assets/{PrBotBehaviourTab-CtfRiaES.js → PrBotBehaviourTab-Bwu839bx.js} +1 -1
- package/public/assets/index-BoS_VHWj.js +81 -0
- package/public/index.html +1 -1
- package/public/assets/ClaudeReviewTab-CGFvyUOI.js +0 -20
- package/public/assets/index-BHxZquEA.js +0 -81
|
@@ -9,6 +9,8 @@ export const RESULT_TOOL_NAMES = [
|
|
|
9
9
|
'submit_pr_card',
|
|
10
10
|
'submit_ci_review',
|
|
11
11
|
'submit_threads',
|
|
12
|
+
// The replies pass (review/claude-review/replies-check.ts): judgements of replies on Limn's findings.
|
|
13
|
+
'submit_replies',
|
|
12
14
|
'submit_design',
|
|
13
15
|
'submit_paths',
|
|
14
16
|
'submit_alignment',
|
|
@@ -30,6 +32,7 @@ const BUILDERS = {
|
|
|
30
32
|
submit_pr_card: async (z) => (await import('../review/ticket-review/schema.js')).buildSubmitPrCardShape(z),
|
|
31
33
|
submit_ci_review: async (z) => (await import('../review/ci-review/schema.js')).buildSubmitCiReviewShape(z),
|
|
32
34
|
submit_threads: async (z) => (await import('../review/claude-review/thread-check-schema.js')).buildSubmitThreadsShape(z),
|
|
35
|
+
submit_replies: async (z) => (await import('../review/claude-review/replies-check-schema.js')).buildSubmitRepliesShape(z),
|
|
33
36
|
submit_design: async (z) => (await import('../review/diagrams/schema.js')).buildSubmitDesignShape(z),
|
|
34
37
|
submit_paths: async (z) => (await import('../review/sprint-paths/schema.js')).buildSubmitPathsShape(z),
|
|
35
38
|
submit_alignment: async (z) => (await import('../review/story-docs/schema.js')).buildSubmitAlignmentShape(z),
|
|
@@ -529,6 +529,11 @@ function tierFor(method, path) {
|
|
|
529
529
|
// "Post reply" on a follow-up reply status: ONE GitHub thread reply (+ a resolve), no model.
|
|
530
530
|
if (mutating && /^\/api\/claude-reviews\/\d+\/follow-up\/\d+\/reply$/.test(path))
|
|
531
531
|
return [TIERS.githubWrite];
|
|
532
|
+
// TEAM CLAIMS "Clear" (review/claude-review/routes.ts): ONE commit-status write, no model. Spelled
|
|
533
|
+
// EXACTLY here, before the Claude Review family below would park it on the AI bucket (the
|
|
534
|
+
// `hitsGithub` alternation further down is never reached by a `/claude-review` path).
|
|
535
|
+
if (mutating && /^\/api\/prs\/\d+\/claude-review\/team-claim\/clear$/.test(path))
|
|
536
|
+
return [TIERS.githubWrite];
|
|
532
537
|
// AI Fix PLAN MODE (coding/ai-fix/routes.ts, LOCAL ONLY). Spelled out so the family cannot drift
|
|
533
538
|
// with the /api/pro/ catch-all below, which would also have parked the DB-only discard on `ai`:
|
|
534
539
|
// POST /api/pro/prs/:id/ai-fix/plan starts a planning agent pass → ai + ai_hourly
|
package/dist/api/routes/prs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { REQUEST_CHANGES_DEFAULT_BODY, WITHDRAW_APPROVAL_MESSAGE } from '../../shared/index.js';
|
|
2
|
+
import { REQUEST_CHANGES_DEFAULT_BODY, WITHDRAW_APPROVAL_MESSAGE, mergeStateWithoutLimn } from '../../shared/index.js';
|
|
3
3
|
import { accountGithubLogin } from '../../db/review-threads-for-review.js';
|
|
4
4
|
import { config } from '../../config.js';
|
|
5
5
|
import { stampPrMergeQueueStateNonFatal } from '../../db/pr-merge-queue-stamp.js';
|
|
@@ -724,7 +724,8 @@ export async function prRoutes(app) {
|
|
|
724
724
|
allowedMethods: [...allowedMethods],
|
|
725
725
|
defaultMethod: allowedMethods[0] ?? 'merge',
|
|
726
726
|
mergeable: m.mergeable,
|
|
727
|
-
|
|
727
|
+
// ⚠ An UNSTABLE only Limn's own status lines caused reads CLEAN (shared limn-status.ts).
|
|
728
|
+
mergeStateStatus: mergeStateWithoutLimn(m.mergeableState, ctx.ciStatus),
|
|
728
729
|
conflicts,
|
|
729
730
|
behind,
|
|
730
731
|
blocked: m.mergeableState === 'blocked',
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// THE SEVERITY CHIP ON LIMN'S OWN POSTED FINDINGS — which review thread / PR-level comment of a PR
|
|
2
|
+
// is a finding Limn posted, and the severity it was posted with. Read inside getPrDetail (the
|
|
3
|
+
// payload the PR details pane already loads), so the chip costs no request of its own.
|
|
4
|
+
//
|
|
5
|
+
// The link is the thread-owner link the rest of Claude Review uses:
|
|
6
|
+
// - a finding's stored `github_comment_id` is the ROOT comment of the thread it started
|
|
7
|
+
// (`posted_comment_kind = 'inline'`) or the PR-level comment it became ('pr_comment');
|
|
8
|
+
// - its latest REMINDER's comment is the same finding, posted again;
|
|
9
|
+
// - an inline-posted finding with no stored id (its review read-back failed) is matched by the ONE
|
|
10
|
+
// shared matcher, `findFindingThread` (same file, the account's own login, the hidden marker,
|
|
11
|
+
// the finding's text at the start).
|
|
12
|
+
// The severity goes through the shared grader seam `severityForPostedFinding`, the same call the
|
|
13
|
+
// comment builders make, so the chip always says what GitHub says. Praise is never posted: no chip.
|
|
14
|
+
import { and, eq, inArray, isNotNull } from 'drizzle-orm';
|
|
15
|
+
import { severityForPostedFinding, } from '../shared/index.js';
|
|
16
|
+
import { db, schema } from './client.js';
|
|
17
|
+
import { findFindingThread } from '../review/claude-review/finding-thread.js';
|
|
18
|
+
import { accountGithubLogin } from './review-threads-for-review.js';
|
|
19
|
+
/** The pure fold (see the header). `threadComments` are every comment of the PR's threads. */
|
|
20
|
+
export function limnSeverityTargets(input) {
|
|
21
|
+
const byThreadId = new Map();
|
|
22
|
+
const byPrCommentId = new Map();
|
|
23
|
+
const inline = new Map();
|
|
24
|
+
const prLevel = new Map();
|
|
25
|
+
const unlinked = [];
|
|
26
|
+
// Oldest first, so the finding that really posted owns its thread before a later one is tried.
|
|
27
|
+
for (const f of [...input.findings].sort((a, b) => a.id - b.id)) {
|
|
28
|
+
const sev = severityForPostedFinding(f);
|
|
29
|
+
if (sev == null)
|
|
30
|
+
continue;
|
|
31
|
+
if (f.githubCommentId != null) {
|
|
32
|
+
(f.postedCommentKind === 'pr_comment' ? prLevel : inline).set(f.githubCommentId, sev);
|
|
33
|
+
}
|
|
34
|
+
else if (f.postedCommentKind !== 'pr_comment') {
|
|
35
|
+
unlinked.push({ f, sev });
|
|
36
|
+
}
|
|
37
|
+
const r = f.reminder;
|
|
38
|
+
if (r?.githubCommentId)
|
|
39
|
+
(r.postedCommentKind === 'pr_comment' ? prLevel : inline).set(r.githubCommentId, sev);
|
|
40
|
+
}
|
|
41
|
+
if (inline.size + prLevel.size + unlinked.length === 0)
|
|
42
|
+
return { byThreadId, byPrCommentId };
|
|
43
|
+
// Each thread's ROOT comment (the earliest).
|
|
44
|
+
const roots = new Map();
|
|
45
|
+
for (const c of input.threadComments) {
|
|
46
|
+
const r = roots.get(c.threadId);
|
|
47
|
+
if (!r || c.createdAt < r.createdAt)
|
|
48
|
+
roots.set(c.threadId, c);
|
|
49
|
+
}
|
|
50
|
+
for (const [threadId, root] of roots) {
|
|
51
|
+
const sev = root.databaseId != null ? inline.get(root.databaseId) : undefined;
|
|
52
|
+
if (sev != null)
|
|
53
|
+
byThreadId.set(threadId, sev);
|
|
54
|
+
}
|
|
55
|
+
const taken = new Set(byThreadId.keys());
|
|
56
|
+
for (const { f, sev } of unlinked) {
|
|
57
|
+
const t = findFindingThread(f, input.threads, input.threadComments, input.accountLogin, taken);
|
|
58
|
+
if (t) {
|
|
59
|
+
byThreadId.set(t.id, sev);
|
|
60
|
+
taken.add(t.id);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (const c of input.prComments) {
|
|
64
|
+
const sev = c.databaseId != null ? prLevel.get(c.databaseId) : undefined;
|
|
65
|
+
if (sev != null)
|
|
66
|
+
byPrCommentId.set(c.id, sev);
|
|
67
|
+
}
|
|
68
|
+
return { byThreadId, byPrCommentId };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Load this PR's posted findings and fold them over the thread / comment rows getPrDetail already
|
|
72
|
+
* read. One indexed query when nothing of Limn's is posted; the account login and the comment
|
|
73
|
+
* authors are read only when a posted inline finding has no stored comment id.
|
|
74
|
+
*/
|
|
75
|
+
export async function loadLimnSeverityTargets(accountId, prId, rows) {
|
|
76
|
+
const { claudeReviews: cr, claudeReviewFindings: crf } = schema;
|
|
77
|
+
const findings = (await db
|
|
78
|
+
.select({
|
|
79
|
+
id: crf.id,
|
|
80
|
+
severity: crf.severity,
|
|
81
|
+
path: crf.path,
|
|
82
|
+
line: crf.line,
|
|
83
|
+
body: crf.body,
|
|
84
|
+
editedBody: crf.editedBody,
|
|
85
|
+
githubCommentId: crf.githubCommentId,
|
|
86
|
+
postedCommentKind: crf.postedCommentKind,
|
|
87
|
+
reminder: crf.reminder,
|
|
88
|
+
})
|
|
89
|
+
.from(crf)
|
|
90
|
+
.innerJoin(cr, eq(cr.id, crf.reviewId))
|
|
91
|
+
.where(and(eq(cr.accountId, accountId), eq(cr.prId, prId), isNotNull(crf.postedAt)))
|
|
92
|
+
.execute());
|
|
93
|
+
if (findings.length === 0)
|
|
94
|
+
return { byThreadId: new Map(), byPrCommentId: new Map() };
|
|
95
|
+
const needsMatcher = findings.some((f) => f.githubCommentId == null && f.postedCommentKind !== 'pr_comment');
|
|
96
|
+
let accountLogin = null;
|
|
97
|
+
const loginById = new Map();
|
|
98
|
+
if (needsMatcher) {
|
|
99
|
+
accountLogin = await accountGithubLogin(accountId);
|
|
100
|
+
const authorIds = [...new Set(rows.threadComments.map((c) => c.authorId).filter((x) => x != null))];
|
|
101
|
+
if (authorIds.length > 0) {
|
|
102
|
+
const us = await db
|
|
103
|
+
.select({ id: schema.users.id, login: schema.users.githubLogin })
|
|
104
|
+
.from(schema.users)
|
|
105
|
+
.where(inArray(schema.users.id, authorIds))
|
|
106
|
+
.execute();
|
|
107
|
+
for (const u of us)
|
|
108
|
+
if (u.login)
|
|
109
|
+
loginById.set(u.id, u.login);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return limnSeverityTargets({
|
|
113
|
+
findings,
|
|
114
|
+
threads: rows.threads,
|
|
115
|
+
threadComments: rows.threadComments.map((c) => ({
|
|
116
|
+
threadId: c.threadId,
|
|
117
|
+
databaseId: c.databaseId,
|
|
118
|
+
body: c.body,
|
|
119
|
+
authorLogin: c.authorId != null ? (loginById.get(c.authorId) ?? null) : null,
|
|
120
|
+
createdAt: c.createdAt.getTime(),
|
|
121
|
+
})),
|
|
122
|
+
prComments: rows.prComments,
|
|
123
|
+
accountLogin,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=limn-finding-severity.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- TEAM CLAIMS (docs/CLAUDE-REVIEW.md § Team claims). Two additive columns:
|
|
2
|
+
-- workspaces.team_claims_enabled "Mark auto reviews on GitHub": an auto review posts a commit
|
|
3
|
+
-- status (limn/review-claude | limn/review-codex) on the PR's head
|
|
4
|
+
-- so teammates' Limn skips a commit someone is already reviewing.
|
|
5
|
+
-- NULL/0 = OFF, the default for every workspace.
|
|
6
|
+
-- claude_reviews.team_claim what the claim did for THIS run (JSON, ClaudeTeamClaimRecord).
|
|
7
|
+
-- NULL = no claim was attempted (a manual run, or the switch off).
|
|
8
|
+
ALTER TABLE `workspaces` ADD `team_claims_enabled` integer;
|
|
9
|
+
--> statement-breakpoint
|
|
10
|
+
ALTER TABLE `claude_reviews` ADD `team_claim` text;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- CLAUDE + CODEX PAIRS (docs/CLAUDE-REVIEW.md § Claude + Codex). A pair is two ordinary runs: Claude
|
|
2
|
+
-- first, then Codex with Claude's findings as already raised. `pair_id` = the FIRST run's id, on both
|
|
3
|
+
-- rows; `pair_role` = 'first' | 'second'; `pair_dropped` = how many of the second run's findings
|
|
4
|
+
-- repeated a first-run finding and were dropped in code (second run only). NULL on every other row.
|
|
5
|
+
ALTER TABLE `claude_reviews` ADD `pair_id` integer;
|
|
6
|
+
--> statement-breakpoint
|
|
7
|
+
ALTER TABLE `claude_reviews` ADD `pair_role` text;
|
|
8
|
+
--> statement-breakpoint
|
|
9
|
+
ALTER TABLE `claude_reviews` ADD `pair_dropped` integer;
|
|
10
|
+
--> statement-breakpoint
|
|
11
|
+
-- Who runs a workspace's AUTO reviews: NULL = the default provider (Settings → AI); 'both' = a pair.
|
|
12
|
+
ALTER TABLE `workspaces` ADD `auto_review_providers` text;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
-- A REPLIES PASS (docs/CLAUDE-REVIEW.md § The replies pass): a light run that judges only people's
|
|
2
|
+
-- replies on Limn's own posted findings, stored as a `claude_reviews` row so history shows it and
|
|
3
|
+
-- its follow-up items read like any review's. 'replies' on such a row; NULL on every full review,
|
|
4
|
+
-- older ones included. Additive only.
|
|
5
|
+
ALTER TABLE `claude_reviews` ADD `kind` text;
|
|
@@ -939,6 +939,27 @@
|
|
|
939
939
|
"when": 1792137620016,
|
|
940
940
|
"tag": "0133_review_fresh",
|
|
941
941
|
"breakpoints": true
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
"idx": 134,
|
|
945
|
+
"version": "6",
|
|
946
|
+
"when": 1792137621016,
|
|
947
|
+
"tag": "0134_team_claims",
|
|
948
|
+
"breakpoints": true
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
"idx": 135,
|
|
952
|
+
"version": "6",
|
|
953
|
+
"when": 1792137622016,
|
|
954
|
+
"tag": "0135_review_pairs",
|
|
955
|
+
"breakpoints": true
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
"idx": 136,
|
|
959
|
+
"version": "6",
|
|
960
|
+
"when": 1792137623016,
|
|
961
|
+
"tag": "0136_review_kind",
|
|
962
|
+
"breakpoints": true
|
|
942
963
|
}
|
|
943
964
|
]
|
|
944
965
|
}
|
package/dist/db/pr-liveness.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// in this batch: queries.ts is already 9k lines and carries literal NUL bytes that make `rg`/
|
|
15
15
|
// `grep` under-report against it, so new folds go in a file a search can actually find.
|
|
16
16
|
import { and, eq, inArray } from 'drizzle-orm';
|
|
17
|
+
import { mergeStateWithoutLimn } from '../shared/index.js';
|
|
17
18
|
import { db, schema } from './client.js';
|
|
18
19
|
import { READY_MERGE_STATES } from './triage.js';
|
|
19
20
|
import { noteLiveMergeTransition } from '../sync/merge-notice.js';
|
|
@@ -44,6 +45,7 @@ export async function getPrLivenessTargets(accountId, repoIds, prIds) {
|
|
|
44
45
|
inMergeQueue: pullRequests.inMergeQueue,
|
|
45
46
|
mergeQueueEntryState: pullRequests.mergeQueueEntryState,
|
|
46
47
|
updatedAt: pullRequests.updatedAt,
|
|
48
|
+
ciStatus: pullRequests.ciStatus,
|
|
47
49
|
})
|
|
48
50
|
.from(pullRequests)
|
|
49
51
|
.where(and(eq(pullRequests.accountId, accountId), inArray(pullRequests.repoId, repoIds), inArray(pullRequests.id, prIds)))
|
|
@@ -60,6 +62,7 @@ export async function getPrLivenessTargets(accountId, repoIds, prIds) {
|
|
|
60
62
|
inMergeQueue: r.inMergeQueue ?? null,
|
|
61
63
|
mergeQueueEntryState: r.mergeQueueEntryState ?? null,
|
|
62
64
|
updatedAt: r.updatedAt,
|
|
65
|
+
ciStatus: r.ciStatus ?? null,
|
|
63
66
|
}));
|
|
64
67
|
}
|
|
65
68
|
/**
|
|
@@ -221,8 +224,14 @@ export async function applyPrLiveness(accountId, target, obs) {
|
|
|
221
224
|
// computation) keeps its previous state until the next full walk. That is the same staleness
|
|
222
225
|
// every card had before this route existed, on a narrow slice of PRs, for a couple of minutes.
|
|
223
226
|
const authoritative = (next, stored, unknown) => next !== undefined && next !== stored && !(next === unknown && stored != null);
|
|
224
|
-
|
|
225
|
-
|
|
227
|
+
// ⚠ LIMN'S OWN LINES ARE NOT CI: an UNSTABLE that only they caused (the stored CI, which never
|
|
228
|
+
// counts them, is green) is written CLEAN — exactly as `persistPr` writes it. A stored CLEAN over
|
|
229
|
+
// a stored 'unknown' CI is `persistPr`'s "only Limn's lines, no other CI" answer (it read the
|
|
230
|
+
// contexts; this probe does not), so an UNSTABLE here does not flip it back.
|
|
231
|
+
const ciForMergeState = target.ciStatus === 'unknown' && target.mergeStateStatus === 'clean' ? 'none' : target.ciStatus;
|
|
232
|
+
const observedMergeState = mergeStateWithoutLimn(obs.mergeStateStatus, ciForMergeState);
|
|
233
|
+
if (authoritative(observedMergeState, target.mergeStateStatus, 'unknown')) {
|
|
234
|
+
set.mergeStateStatus = observedMergeState;
|
|
226
235
|
change(true);
|
|
227
236
|
}
|
|
228
237
|
if (authoritative(obs.mergeable, target.mergeable, 'unknown')) {
|
package/dist/db/queries.js
CHANGED
|
@@ -47,6 +47,7 @@ import { hubReadingFor, loadRepoCoupling } from './file-coupling.js';
|
|
|
47
47
|
import { DAYS_PER_MONTH, botWindowMs } from './bot-window.js';
|
|
48
48
|
import { dormantBotUserIds } from './bot-dormancy.js';
|
|
49
49
|
import { newestReviewCommentTimes } from './review-threads-for-review.js';
|
|
50
|
+
import { loadLimnSeverityTargets } from './limn-finding-severity.js';
|
|
50
51
|
import { automationVendorUserIds, exactAutomationVendorUsers, githubTypeBotUserIds, globalAutomationUserIds, } from './automation-ids.js';
|
|
51
52
|
import { forwardKindOf, isConflicting, isRedCiStatus, RED_CI_STATUSES, } from './pending-classify.js';
|
|
52
53
|
import { authorAutomationFor, dependencyPrState, dependencyStateDetail, MERGE_QUEUE_CARD_DETAIL, securityFixDetail, unionAdvisoryIds, } from './dependency-cards.js';
|
|
@@ -7289,6 +7290,12 @@ export async function getPrDetail(id, accountId) {
|
|
|
7289
7290
|
arr.push(c);
|
|
7290
7291
|
commentsByThread.set(c.threadId, arr);
|
|
7291
7292
|
}
|
|
7293
|
+
// Limn's own posted findings: the severity chip on their threads and PR-level comments.
|
|
7294
|
+
const limnSeverity = await loadLimnSeverityTargets(accountId, id, {
|
|
7295
|
+
threads: threadRows,
|
|
7296
|
+
threadComments: commentRows,
|
|
7297
|
+
prComments: prCommentRows,
|
|
7298
|
+
});
|
|
7292
7299
|
const threads = threadRows.map((t) => {
|
|
7293
7300
|
const tComments = commentsByThread.get(t.id) ?? [];
|
|
7294
7301
|
return {
|
|
@@ -7317,6 +7324,7 @@ export async function getPrDetail(id, accountId) {
|
|
|
7317
7324
|
url: tComments[0]?.databaseId
|
|
7318
7325
|
? `${prUrl}#discussion_r${tComments[0].databaseId}`
|
|
7319
7326
|
: null,
|
|
7327
|
+
limnSeverity: limnSeverity.byThreadId.get(t.id) ?? null,
|
|
7320
7328
|
};
|
|
7321
7329
|
});
|
|
7322
7330
|
// Bot-triage provenance (compute-on-read): which reviews were posted by an automated
|
|
@@ -7360,6 +7368,7 @@ export async function getPrDetail(id, accountId) {
|
|
|
7360
7368
|
body: c.body ?? '',
|
|
7361
7369
|
createdAt: c.createdAt.toISOString(),
|
|
7362
7370
|
url: c.databaseId ? `${prUrl}#issuecomment-${c.databaseId}` : null,
|
|
7371
|
+
limnSeverity: limnSeverity.byPrCommentId.get(c.id) ?? null,
|
|
7363
7372
|
}));
|
|
7364
7373
|
const commitsOut = commitRows.map((c) => ({
|
|
7365
7374
|
id: c.id,
|
|
@@ -8033,7 +8042,9 @@ repoIds) {
|
|
|
8033
8042
|
.from(claudeReviews)
|
|
8034
8043
|
.innerJoin(pullRequests, eq(pullRequests.id, claudeReviews.prId))
|
|
8035
8044
|
.innerJoin(repos, eq(repos.id, pullRequests.repoId))
|
|
8036
|
-
.where(and(eq(repos.accountId, accountId), repoIds == null ? undefined : inArray(pullRequests.repoId, repoIds), eq(claudeReviews.status, 'succeeded'), eq(pullRequests.state, 'open')
|
|
8045
|
+
.where(and(eq(repos.accountId, accountId), repoIds == null ? undefined : inArray(pullRequests.repoId, repoIds), eq(claudeReviews.status, 'succeeded'), eq(pullRequests.state, 'open'),
|
|
8046
|
+
// A REPLIES PASS (kind 'replies') is not a review to action: the card follows the full one.
|
|
8047
|
+
isNull(claudeReviews.kind)))
|
|
8037
8048
|
.orderBy(desc(claudeReviews.finishedAt), desc(claudeReviews.createdAt))
|
|
8038
8049
|
.execute();
|
|
8039
8050
|
const seen = new Set(); // most-recent succeeded run per PR only
|
|
@@ -8145,7 +8156,12 @@ export async function getAutoReviewCandidates(accountId, workspaceId, opts) {
|
|
|
8145
8156
|
.select({ id: claudeReviews.id })
|
|
8146
8157
|
.from(claudeReviews)
|
|
8147
8158
|
.innerJoin(pullRequests, eq(pullRequests.id, claudeReviews.prId))
|
|
8148
|
-
.where(and(eq(claudeReviews.accountId, accountId), eq(claudeReviews.trigger, 'auto'), gte(claudeReviews.createdAt, new Date(opts.dayStartMs)), inArray(pullRequests.repoId, repoIds)
|
|
8159
|
+
.where(and(eq(claudeReviews.accountId, accountId), eq(claudeReviews.trigger, 'auto'), gte(claudeReviews.createdAt, new Date(opts.dayStartMs)), inArray(pullRequests.repoId, repoIds),
|
|
8160
|
+
// Replies passes have their own per-PR cap (review/claude-review/replies-check.ts).
|
|
8161
|
+
isNull(claudeReviews.kind),
|
|
8162
|
+
// A CLAUDE + CODEX PAIR is ONE review of the PR: it counts once toward the daily cap (its
|
|
8163
|
+
// first half's row), never twice.
|
|
8164
|
+
or(isNull(claudeReviews.pairRole), ne(claudeReviews.pairRole, 'second'))))
|
|
8149
8165
|
.execute();
|
|
8150
8166
|
const autoToday = autoRows.length;
|
|
8151
8167
|
if (opts.limit <= 0)
|
|
@@ -8163,7 +8179,7 @@ export async function getAutoReviewCandidates(accountId, workspaceId, opts) {
|
|
|
8163
8179
|
.execute();
|
|
8164
8180
|
if (open.length === 0)
|
|
8165
8181
|
return { prIds: [], autoToday, reReview: [], inFlightMoved: [], retry: [] };
|
|
8166
|
-
const [bots,
|
|
8182
|
+
const [bots, allRuns, reviewerBots] = await Promise.all([
|
|
8167
8183
|
hiddenBotUserIds(accountId, workspaceId),
|
|
8168
8184
|
db
|
|
8169
8185
|
.select({
|
|
@@ -8178,6 +8194,8 @@ export async function getAutoReviewCandidates(accountId, workspaceId, opts) {
|
|
|
8178
8194
|
error: claudeReviews.error,
|
|
8179
8195
|
provider: claudeReviews.provider,
|
|
8180
8196
|
model: claudeReviews.model,
|
|
8197
|
+
kind: claudeReviews.kind,
|
|
8198
|
+
pairId: claudeReviews.pairId,
|
|
8181
8199
|
})
|
|
8182
8200
|
.from(claudeReviews)
|
|
8183
8201
|
.where(and(eq(claudeReviews.accountId, accountId), inArray(claudeReviews.prId, open.map((r) => r.id))))
|
|
@@ -8185,6 +8203,11 @@ export async function getAutoReviewCandidates(accountId, workspaceId, opts) {
|
|
|
8185
8203
|
automatedReviewerUserIds(accountId, workspaceId, 'review'),
|
|
8186
8204
|
]);
|
|
8187
8205
|
const botSet = new Set(bots);
|
|
8206
|
+
// ⚠ A REPLIES PASS (kind 'replies') is NOT a review: it neither settles a head nor counts as a
|
|
8207
|
+
// first review, a retry or a run in flight. Only its coverage of the own-finding replies is read
|
|
8208
|
+
// (`repliesCoveredMs`, below). Every rule from here on reads full reviews alone.
|
|
8209
|
+
const repliesRows = allRuns.filter((r) => r.kind === 'replies');
|
|
8210
|
+
const reviewed = allRuns.filter((r) => r.kind == null);
|
|
8188
8211
|
const hasRun = new Set(reviewed.map((r) => r.prId));
|
|
8189
8212
|
const human = open.filter((r) => r.authorId != null && !botSet.has(r.authorId));
|
|
8190
8213
|
// ⚠ THE ONBOARDING FLOOR GATES FIRST REVIEWS ONLY. A PR opened before the switch is never
|
|
@@ -8290,7 +8313,15 @@ export async function getAutoReviewCandidates(accountId, workspaceId, opts) {
|
|
|
8290
8313
|
// else its START (`created_at`) for a row that never recorded one — never 0, or a deploy would
|
|
8291
8314
|
// re-review every PR that already has comments.
|
|
8292
8315
|
const coveredMs = Math.max(...atHead.map((r) => (r.commentsThrough ?? r.createdAt).getTime()));
|
|
8293
|
-
|
|
8316
|
+
// How far the replies passes AT THIS HEAD reached (any status: one attempt per burst).
|
|
8317
|
+
const passes = repliesRows.filter((r) => r.prId === pr.id && r.headSha === pr.headSha);
|
|
8318
|
+
const repliesCoveredMs = passes.length > 0 ? Math.max(...passes.map((r) => (r.commentsThrough ?? r.createdAt).getTime())) : null;
|
|
8319
|
+
// ⚠ OWN-FINDING REPLIES and a CLAUDE + CODEX PAIR: each half follows up ONLY its own provider's
|
|
8320
|
+
// findings, so a reply on a Claude finding that landed while the Claude half ran is NOT judged by
|
|
8321
|
+
// the Codex half, though that half's coverage reaches past it. A pair therefore covers own-thread
|
|
8322
|
+
// replies only as far as its EARLIER-reaching half (`pairOwnCoveredMs`); an ordinary run as above.
|
|
8323
|
+
const ownCoveredMs = pairOwnCoveredMs(atHead);
|
|
8324
|
+
sameHead.push({ prId: pr.id, headSha: pr.headSha, coveredMs, ownCoveredMs, repliesCoveredMs });
|
|
8294
8325
|
}
|
|
8295
8326
|
// RE-REVIEW ON NEW COMMENTS: a qualifying review-thread comment (a person's or a bot's, in an
|
|
8296
8327
|
// unresolved thread, never one Limn posted — db/review-threads-for-review.ts) newer than what
|
|
@@ -8334,8 +8365,13 @@ export async function getAutoReviewCandidates(accountId, workspaceId, opts) {
|
|
|
8334
8365
|
const ck = latestCheck.get(p.prId);
|
|
8335
8366
|
const checkCoveredMs = ck && ck.headSha === p.headSha ? (ck.commentsThrough ?? ck.createdAt).getTime() : null;
|
|
8336
8367
|
const ownMs = t.newestOwn?.getTime() ?? null;
|
|
8337
|
-
|
|
8338
|
-
|
|
8368
|
+
// A reply on Limn's own finding goes to a REPLIES PASS (review/claude-review/auto.ts
|
|
8369
|
+
// `commentBurstRoute`), which covers it once a pass at this head reached it.
|
|
8370
|
+
const ownNeedsPass = ownMs != null && ownMs > Math.max(p.ownCoveredMs, p.repliesCoveredMs ?? 0);
|
|
8371
|
+
// Everything else goes to Check threads, as before.
|
|
8372
|
+
const otherMs = t.newestOther?.getTime() ?? null;
|
|
8373
|
+
const otherNeedsCheck = otherMs != null && otherMs > Math.max(p.coveredMs, checkCoveredMs ?? 0);
|
|
8374
|
+
if (!ownNeedsPass && !otherNeedsCheck)
|
|
8339
8375
|
continue;
|
|
8340
8376
|
reReview.push({
|
|
8341
8377
|
prId: p.prId,
|
|
@@ -8344,8 +8380,11 @@ export async function getAutoReviewCandidates(accountId, workspaceId, opts) {
|
|
|
8344
8380
|
reason: 'comments',
|
|
8345
8381
|
lastRunAtMs: null,
|
|
8346
8382
|
coveredMs: p.coveredMs,
|
|
8383
|
+
ownCoveredMs: p.ownCoveredMs,
|
|
8347
8384
|
checkCoveredMs,
|
|
8348
8385
|
newestOwnThreadCommentAtMs: ownMs,
|
|
8386
|
+
repliesCoveredMs: p.repliesCoveredMs,
|
|
8387
|
+
newestOtherCommentAtMs: otherMs,
|
|
8349
8388
|
});
|
|
8350
8389
|
if (reReview.length >= opts.limit)
|
|
8351
8390
|
break;
|
|
@@ -8353,6 +8392,27 @@ export async function getAutoReviewCandidates(accountId, workspaceId, opts) {
|
|
|
8353
8392
|
}
|
|
8354
8393
|
return { prIds, autoToday, reReview, inFlightMoved, retry };
|
|
8355
8394
|
}
|
|
8395
|
+
/**
|
|
8396
|
+
* How far the full reviews at one head covered replies on Limn's OWN findings: the max over runs of
|
|
8397
|
+
* (comments_through ?? created_at), except that the two halves of a Claude + Codex pair (same
|
|
8398
|
+
* `pairId`) count as their EARLIER-reaching half — each half follows up only its own provider's
|
|
8399
|
+
* findings, so the later half's coverage says nothing about the other half's threads. Pure.
|
|
8400
|
+
*/
|
|
8401
|
+
export function pairOwnCoveredMs(runs) {
|
|
8402
|
+
const byPair = new Map();
|
|
8403
|
+
let best = 0;
|
|
8404
|
+
for (const r of runs) {
|
|
8405
|
+
const t = (r.commentsThrough ?? r.createdAt).getTime();
|
|
8406
|
+
if (r.pairId == null) {
|
|
8407
|
+
best = Math.max(best, t);
|
|
8408
|
+
continue;
|
|
8409
|
+
}
|
|
8410
|
+
byPair.set(r.pairId, Math.min(byPair.get(r.pairId) ?? Number.POSITIVE_INFINITY, t));
|
|
8411
|
+
}
|
|
8412
|
+
for (const t of byPair.values())
|
|
8413
|
+
best = Math.max(best, t);
|
|
8414
|
+
return best;
|
|
8415
|
+
}
|
|
8356
8416
|
// WHO MAY RE-TRIGGER AN AUTO REVIEW BY COMMENTING (the comment half of the re-review key). A
|
|
8357
8417
|
// PERSON — not in the workspace's bot union `hiddenBotUserIds`, where a manual "this is a human"
|
|
8358
8418
|
// wins both ways — or a bot whose role in THIS workspace is `'review'` (`automatedReviewerUserIds`
|
|
@@ -8716,6 +8776,7 @@ export async function getPrWriteContext(prId, accountId) {
|
|
|
8716
8776
|
viewerPermission: repos.viewerPermission,
|
|
8717
8777
|
mergeStateStatus: pullRequests.mergeStateStatus,
|
|
8718
8778
|
reviewDecision: pullRequests.reviewDecision,
|
|
8779
|
+
ciStatus: pullRequests.ciStatus,
|
|
8719
8780
|
})
|
|
8720
8781
|
.from(pullRequests)
|
|
8721
8782
|
.innerJoin(repos, eq(repos.id, pullRequests.repoId))
|
|
@@ -8738,6 +8799,7 @@ export async function getPrWriteContext(prId, accountId) {
|
|
|
8738
8799
|
prUrl: `https://github.com/${row.owner}/${row.name}/pull/${row.number}`,
|
|
8739
8800
|
mergeStateStatus: row.mergeStateStatus ?? null,
|
|
8740
8801
|
reviewDecision: row.reviewDecision ?? null,
|
|
8802
|
+
ciStatus: row.ciStatus ?? null,
|
|
8741
8803
|
};
|
|
8742
8804
|
}
|
|
8743
8805
|
// Optimistically stamp a PR as merged after a successful GitHub merge, so the UI reflects it
|
|
@@ -112,7 +112,8 @@ countsAuthor) {
|
|
|
112
112
|
/**
|
|
113
113
|
* `newestReviewCommentAt`, plus `newestOwn`: the newest of those comments inside a thread ROOTED on
|
|
114
114
|
* one of Limn's own posted findings (the loader's `newestOwnThreadCommentAt`, same filter) — the
|
|
115
|
-
* sweeper's "a reply on a Limn finding
|
|
115
|
+
* sweeper's "a reply on a Limn finding goes to a replies pass" test, read in the same scan — and
|
|
116
|
+
* `newestOther`: the newest one in any OTHER thread (Check threads' half).
|
|
116
117
|
*/
|
|
117
118
|
export async function newestReviewCommentTimes(accountId, prIds, since, countsAuthor) {
|
|
118
119
|
const out = new Map();
|
|
@@ -183,13 +184,15 @@ export async function newestReviewCommentTimes(accountId, prIds, since, countsAu
|
|
|
183
184
|
const prev = out.get(r.prId);
|
|
184
185
|
const isOwn = r.threadId != null && ownRooted.has(r.threadId);
|
|
185
186
|
if (!prev) {
|
|
186
|
-
out.set(r.prId, { newest: r.createdAt, newestOwn: isOwn ? r.createdAt : null });
|
|
187
|
+
out.set(r.prId, { newest: r.createdAt, newestOwn: isOwn ? r.createdAt : null, newestOther: isOwn ? null : r.createdAt });
|
|
187
188
|
continue;
|
|
188
189
|
}
|
|
189
190
|
if (r.createdAt.getTime() > prev.newest.getTime())
|
|
190
191
|
prev.newest = r.createdAt;
|
|
191
192
|
if (isOwn && (!prev.newestOwn || r.createdAt.getTime() > prev.newestOwn.getTime()))
|
|
192
193
|
prev.newestOwn = r.createdAt;
|
|
194
|
+
if (!isOwn && (!prev.newestOther || r.createdAt.getTime() > prev.newestOther.getTime()))
|
|
195
|
+
prev.newestOther = r.createdAt;
|
|
193
196
|
}
|
|
194
197
|
return out;
|
|
195
198
|
}
|
package/dist/db/schema.sqlite.js
CHANGED
|
@@ -1103,6 +1103,20 @@ export const claudeReviews = sqliteTable('claude_reviews', {
|
|
|
1103
1103
|
// A FROM-SCRATCH run (`GenerateReviewBody.fresh`): it read no earlier review — no follow-up, no
|
|
1104
1104
|
// settled list, no carried thread judgements. true on such a run; NULL otherwise (migration 0133).
|
|
1105
1105
|
fresh: integer('fresh', { mode: 'boolean' }),
|
|
1106
|
+
// WHAT THE TEAM CLAIM DID FOR THIS RUN (migration 0134; review/claude-review/team-claim.ts): the
|
|
1107
|
+
// GitHub status line it posted ("Marked on GitHub"), or why it ran without one. NULL = no claim
|
|
1108
|
+
// was attempted (a manual run, or "Mark auto reviews on GitHub" off).
|
|
1109
|
+
teamClaim: text('team_claim', { mode: 'json' }).$type(),
|
|
1110
|
+
// A CLAUDE + CODEX PAIR (migration 0135; docs/CLAUDE-REVIEW.md § Claude + Codex): `pairId` = the
|
|
1111
|
+
// FIRST run's id, on both halves; `pairRole` 'first' | 'second'; `pairDropped` = the second run's
|
|
1112
|
+
// findings dropped in code as repeats of the first's. NULL on every ordinary run.
|
|
1113
|
+
pairId: integer('pair_id'),
|
|
1114
|
+
pairRole: text('pair_role', { enum: ['first', 'second'] }),
|
|
1115
|
+
pairDropped: integer('pair_dropped'),
|
|
1116
|
+
// A REPLIES PASS (review/claude-review/replies-check.ts): 'replies' on a run that judged only
|
|
1117
|
+
// people's replies on Limn's own posted findings; NULL on every full review (migration 0136).
|
|
1118
|
+
// ⚠ Every "the latest review" / "a run at this head" reader filters `kind IS NULL`.
|
|
1119
|
+
kind: text('kind', { enum: ['replies'] }),
|
|
1106
1120
|
}, (t) => ({
|
|
1107
1121
|
prIdx: index('cr_pr_idx').on(t.prId),
|
|
1108
1122
|
prShaIdx: index('cr_pr_sha_idx').on(t.prId, t.headSha),
|
|
@@ -1768,6 +1782,15 @@ export const workspaces = sqliteTable('workspaces', {
|
|
|
1768
1782
|
// is off. Off stops only the automatic draw; Draw / Redraw still works. The ONE writer is
|
|
1769
1783
|
// `setWorkspaceAutoReview` (review/claude-review/auto-settings.ts).
|
|
1770
1784
|
autoDiagramsEnabled: integer('auto_diagrams_enabled', { mode: 'boolean' }).notNull().default(true),
|
|
1785
|
+
// TEAM CLAIMS (migration 0134; docs/CLAUDE-REVIEW.md § Team claims): "Mark auto reviews on
|
|
1786
|
+
// GitHub". An auto review posts a commit status (limn/review-claude | limn/review-codex) on the
|
|
1787
|
+
// PR's head so teammates' Limn skips a commit someone is already reviewing with the same
|
|
1788
|
+
// provider. NULL/false = OFF, the default. The ONE writer is `setWorkspaceAutoReview`.
|
|
1789
|
+
teamClaimsEnabled: integer('team_claims_enabled', { mode: 'boolean' }),
|
|
1790
|
+
// WHO RUNS AUTO REVIEWS (migration 0135): NULL = the default provider (Settings → AI); 'both' =
|
|
1791
|
+
// Claude then Codex as one pair (docs/CLAUDE-REVIEW.md § Claude + Codex). The ONE writer is
|
|
1792
|
+
// `setWorkspaceAutoReview` (review/claude-review/auto-settings.ts).
|
|
1793
|
+
autoReviewProviders: text('auto_review_providers'),
|
|
1771
1794
|
// DEPENDENCY AUTO-MERGE (migration 0093 / pg 0080): may the server arm "merge when ready" on
|
|
1772
1795
|
// every open dependency-automation PR in this workspace's repos (merge/dependency-policy.ts)?
|
|
1773
1796
|
// NULL/false = OFF, the default for every workspace; nullable with no default, so no insert
|
|
@@ -37,6 +37,16 @@
|
|
|
37
37
|
// re-fires every 120s for a hot repo. So the failing-check DETAIL is a SECOND query
|
|
38
38
|
// (`buildCommitChecksQuery`) issued only for the commits whose rollup phase 1 already reported
|
|
39
39
|
// as failure/error/pending. A green trunk therefore costs exactly the phase-1 walk.
|
|
40
|
+
// LIMN'S OWN STATUS LINES ARE NOT CI (shared limn-status.ts). The commit's legacy STATUS names — a
|
|
41
|
+
// plain list, NOT a connection, so it costs ZERO rate-limit points at any alias count — say whether
|
|
42
|
+
// one of Limn's `limn/review-*` lines is folded into the rollup state beside it. Only then is the
|
|
43
|
+
// state recomputed from the commit's contexts (sync/limn-ci.ts), so a commit without one costs
|
|
44
|
+
// exactly what it did.
|
|
45
|
+
export const LIMN_MARK_FIELDS = /* GraphQL */ `status {
|
|
46
|
+
contexts {
|
|
47
|
+
context
|
|
48
|
+
}
|
|
49
|
+
}`;
|
|
40
50
|
// One trunk-commit node, shared VERBATIM between the live snapshot query and the history
|
|
41
51
|
// backfill below — the two walks must store identical rows, so the selection is written once.
|
|
42
52
|
//
|
|
@@ -54,6 +64,7 @@ const BRANCH_COMMIT_FIELDS = /* GraphQL */ `
|
|
|
54
64
|
statusCheckRollup {
|
|
55
65
|
state
|
|
56
66
|
}
|
|
67
|
+
${LIMN_MARK_FIELDS}
|
|
57
68
|
author {
|
|
58
69
|
name
|
|
59
70
|
avatarUrl
|
|
@@ -152,6 +163,9 @@ const COMMIT_CHECKS_SELECTION = /* GraphQL */ `
|
|
|
152
163
|
statusCheckRollup {
|
|
153
164
|
state
|
|
154
165
|
contexts(first: 100) {
|
|
166
|
+
pageInfo {
|
|
167
|
+
hasNextPage
|
|
168
|
+
}
|
|
155
169
|
nodes {
|
|
156
170
|
__typename
|
|
157
171
|
... on CheckRun {
|
|
@@ -215,7 +229,7 @@ export const COMMIT_STATES_ALIAS_CAP = 100;
|
|
|
215
229
|
* `GitObjectID!` variables, never interpolated into the query text. */
|
|
216
230
|
export function buildCommitStatesQuery(count) {
|
|
217
231
|
const vars = Array.from({ length: count }, (_, i) => `$s${i}: GitObjectID!`).join(', ');
|
|
218
|
-
const aliases = Array.from({ length: count }, (_, i) => ` c${i}: object(oid: $s${i}) { ... on Commit { oid statusCheckRollup { state } } }`).join('\n');
|
|
232
|
+
const aliases = Array.from({ length: count }, (_, i) => ` c${i}: object(oid: $s${i}) { ... on Commit { oid statusCheckRollup { state } ${LIMN_MARK_FIELDS} } }`).join('\n');
|
|
219
233
|
return `query CommitStates($owner: String!, $name: String!, ${vars}) {
|
|
220
234
|
rateLimit {
|
|
221
235
|
cost
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Exported because sync/branch-status.ts maps the SAME contexts union for default-branch
|
|
2
|
+
// commits. The CheckRun status/conclusion → CheckRunState table must not exist twice: if trunk
|
|
3
|
+
// and PR surfaces disagreed about what "failing" means, the same check would render differently
|
|
4
|
+
// depending on where you looked at it.
|
|
5
|
+
export function checkContextState(c) {
|
|
6
|
+
if (c.__typename === 'StatusContext') {
|
|
7
|
+
switch ((c.state ?? '').toUpperCase()) {
|
|
8
|
+
case 'SUCCESS':
|
|
9
|
+
return 'success';
|
|
10
|
+
case 'FAILURE':
|
|
11
|
+
return 'failure';
|
|
12
|
+
case 'ERROR':
|
|
13
|
+
return 'error';
|
|
14
|
+
case 'PENDING':
|
|
15
|
+
case 'EXPECTED':
|
|
16
|
+
return 'pending';
|
|
17
|
+
default:
|
|
18
|
+
return 'unknown';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// CheckRun: outcome is meaningful only once completed.
|
|
22
|
+
if ((c.status ?? '').toUpperCase() !== 'COMPLETED')
|
|
23
|
+
return 'pending';
|
|
24
|
+
switch ((c.conclusion ?? '').toUpperCase()) {
|
|
25
|
+
case 'SUCCESS':
|
|
26
|
+
return 'success';
|
|
27
|
+
case 'FAILURE':
|
|
28
|
+
case 'TIMED_OUT':
|
|
29
|
+
case 'STARTUP_FAILURE':
|
|
30
|
+
case 'ACTION_REQUIRED':
|
|
31
|
+
return 'failure';
|
|
32
|
+
case 'CANCELLED':
|
|
33
|
+
case 'STALE':
|
|
34
|
+
case 'NEUTRAL':
|
|
35
|
+
return 'neutral';
|
|
36
|
+
case 'SKIPPED':
|
|
37
|
+
return 'skipped';
|
|
38
|
+
default:
|
|
39
|
+
return 'unknown';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=check-context-state.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { checkRunsFrom } from '../sync/upsert.js';
|
|
2
|
+
import { rollupStateWithoutLimn } from '../shared/index.js';
|
|
2
3
|
import { getGraphqlClientFor, ghRestGetFor, graphqlTolerant, isRateLimitError } from './client.js';
|
|
3
4
|
import { isLimited, noteLimited } from './rate-budget.js';
|
|
4
5
|
const SHA_RE = /^[0-9a-fA-F]{7,64}$/;
|
|
@@ -12,6 +13,9 @@ export const COMMIT_CHECKS_QUERY = /* GraphQL */ `
|
|
|
12
13
|
statusCheckRollup {
|
|
13
14
|
state
|
|
14
15
|
contexts(first: 100) {
|
|
16
|
+
pageInfo {
|
|
17
|
+
hasNextPage
|
|
18
|
+
}
|
|
15
19
|
nodes {
|
|
16
20
|
__typename
|
|
17
21
|
... on CheckRun {
|
|
@@ -55,11 +59,18 @@ export async function fetchCommitChecks(token, args) {
|
|
|
55
59
|
if (rollup.contexts == null && partial)
|
|
56
60
|
return { ok: false, reason: 'no_check_access' };
|
|
57
61
|
const nodes = (rollup.contexts?.nodes ?? []).filter((n) => n != null);
|
|
58
|
-
const
|
|
62
|
+
const all = checkRunsFrom({
|
|
59
63
|
oid: obj.oid ?? args.sha,
|
|
60
64
|
statusCheckRollup: { state: rollup.state ?? '', contexts: { nodes } },
|
|
61
65
|
});
|
|
62
|
-
|
|
66
|
+
// ⚠ LIMN'S OWN STATUS LINES ARE NOT CI (shared limn-status.ts): the CI review and every other
|
|
67
|
+
// reader of this read see neither the lines nor their effect on the rollup.
|
|
68
|
+
const checks = all.filter((c) => !c.limnOwned);
|
|
69
|
+
return {
|
|
70
|
+
ok: true,
|
|
71
|
+
rollupState: rollupStateWithoutLimn(rollup.state, all, rollup.contexts?.pageInfo?.hasNextPage !== true),
|
|
72
|
+
checks,
|
|
73
|
+
};
|
|
63
74
|
}
|
|
64
75
|
catch (err) {
|
|
65
76
|
const rl = isRateLimitError(err);
|