pierre-review 0.1.71 → 0.1.73
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/api/routes/bot-triage.js +161 -0
- package/dist/api/routes/prs.js +34 -1
- package/dist/app.js +4 -0
- package/dist/bot-triage/auto-triage.js +63 -0
- package/dist/bot-triage/resolve.js +37 -0
- package/dist/db/migrations/0027_user_github_type.sql +6 -0
- package/dist/db/migrations/0028_bot_review_classification.sql +21 -0
- package/dist/db/migrations/0029_bot_mute_rules.sql +19 -0
- package/dist/db/migrations/meta/_journal.json +21 -0
- package/dist/db/migrations-pg/0016_cute_ghost_rider.sql +30 -0
- package/dist/db/migrations-pg/meta/0016_snapshot.json +2774 -0
- package/dist/db/migrations-pg/meta/_journal.json +7 -0
- package/dist/db/queries.js +1419 -13
- package/dist/db/schema.pg.js +45 -0
- package/dist/db/schema.sqlite.js +51 -0
- package/dist/github/queries.js +20 -0
- package/dist/review/post-seam.js +8 -2
- package/dist/sync/app-attribution.js +29 -0
- package/dist/sync/bot-detection.js +91 -2
- package/dist/sync/review-fingerprint.js +112 -0
- package/dist/sync/reviewer-behavior.js +103 -0
- package/dist/sync/reviewer-classify.js +275 -0
- package/dist/sync/scheduler.js +20 -0
- package/dist/sync/upsert.js +54 -1
- package/package.json +1 -1
- package/public/assets/{AiFixTab-B23-MMEW.js → AiFixTab-D_ACiODs.js} +1 -1
- package/public/assets/{ClaudeReviewTab-y5uOQ5r8.js → ClaudeReviewTab-DidHqzvf.js} +1 -1
- package/public/assets/index-BlRvtQJd.js +53 -0
- package/public/assets/index-X4Ro8Jjo.css +1 -0
- package/public/index.html +2 -2
- package/public-landing/assets/{index-DoT9NPaW.js → index-C8_MOIpM.js} +9 -9
- package/public-landing/assets/index-Lj-kzO3U.css +1 -0
- package/public-landing/index.html +8 -8
- package/public-landing/og-image.png +0 -0
- package/public-landing/shots/activity-feed-pro.png +0 -0
- package/public-landing/shots/activity-feed.png +0 -0
- package/public-landing/shots/bot-dedup.png +0 -0
- package/public-landing/shots/bot-inhouse.png +0 -0
- package/public-landing/shots/bot-only-review.png +0 -0
- package/public-landing/shots/bot-review.png +0 -0
- package/public-landing/shots/bot-roi.png +0 -0
- package/public-landing/shots/bot-settings.png +0 -0
- package/public-landing/shots/claude-review.png +0 -0
- package/public-landing/shots/flow-metrics.png +0 -0
- package/public-landing/shots/flow-review-2-memory.png +0 -0
- package/public-landing/shots/flow-review-3-findings.png +0 -0
- package/public-landing/shots/flow-review-4-post.png +0 -0
- package/public-landing/shots/insights.png +0 -0
- package/public-landing/shots/open-pr-strip.png +0 -0
- package/public-landing/shots/pr-detail.png +0 -0
- package/public-landing/shots/repo-console-free.png +0 -0
- package/public-landing/shots/repo-console.png +0 -0
- package/public-landing/shots/settings.png +0 -0
- package/public-landing/shots/sprint-report.png +0 -0
- package/public-landing/shots/timeline.png +0 -0
- package/public/assets/index-BplNgl1k.js +0 -49
- package/public/assets/index-DKZfvKB1.css +0 -1
- package/public-landing/assets/index-HoUbJF4i.css +0 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { addBotMuteRule, deleteBotMuteRule, getBotAnalytics, getBotVendorPrs, getBotDedupClusters, listBotMuteRules, listDetectedReviewers, setReviewerOverride, } from '../../db/queries.js';
|
|
2
|
+
import { accountIdOf } from '../plugins/auth.js';
|
|
3
|
+
// PATCH /api/bot-reviewers/:userId — the two-way manual override. `kind`/`label` are left as
|
|
4
|
+
// open strings (nullable) rather than an enum: AutomatedReviewerKind is defined in the
|
|
5
|
+
// types-only shared package the backend can't import at runtime, and the query layer coerces
|
|
6
|
+
// an unknown kind to a sensible default, so pinning the vendor list here would only add drift.
|
|
7
|
+
const overrideSchema = {
|
|
8
|
+
params: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
required: ['userId'],
|
|
11
|
+
properties: { userId: { type: 'integer' } },
|
|
12
|
+
},
|
|
13
|
+
body: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
required: ['automated'],
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
properties: {
|
|
18
|
+
automated: { type: 'boolean' },
|
|
19
|
+
kind: { type: ['string', 'null'] },
|
|
20
|
+
label: { type: ['string', 'null'] },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
// GET /api/bot-analytics?window= — the window is a closed 4-value set, safe to enum + default.
|
|
25
|
+
const analyticsSchema = {
|
|
26
|
+
querystring: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
additionalProperties: false,
|
|
29
|
+
properties: {
|
|
30
|
+
window: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
enum: ['rolling_7', 'rolling_14', 'rolling_30', 'sprint'],
|
|
33
|
+
default: 'rolling_14',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
// GET /api/bot-analytics/:kind/prs?window= — per-vendor PR drill-down. `kind` is left an open
|
|
39
|
+
// string (AutomatedReviewerKind is types-only shared, unimportable at runtime; the query layer
|
|
40
|
+
// coerces an unknown kind); the window reuses the same closed enum/default as /api/bot-analytics.
|
|
41
|
+
const vendorPrsSchema = {
|
|
42
|
+
params: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
required: ['kind'],
|
|
45
|
+
properties: { kind: { type: 'string' } },
|
|
46
|
+
},
|
|
47
|
+
querystring: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
additionalProperties: false,
|
|
50
|
+
properties: {
|
|
51
|
+
window: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
enum: ['rolling_7', 'rolling_14', 'rolling_30', 'sprint'],
|
|
54
|
+
default: 'rolling_14',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
const prIdParamSchema = {
|
|
60
|
+
params: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
required: ['id'],
|
|
63
|
+
properties: { id: { type: 'integer' } },
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
// POST /api/bot-mute-rules — `action` is a closed enum; `autoResolveDays` is floored at 1 so a
|
|
67
|
+
// 0/negative-day auto-resolve rule (which would immediately clear everything) is rejected.
|
|
68
|
+
const muteRuleSchema = {
|
|
69
|
+
body: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
required: ['action'],
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
properties: {
|
|
74
|
+
vendorKind: { type: ['string', 'null'] },
|
|
75
|
+
pathGlob: { type: ['string', 'null'] },
|
|
76
|
+
severity: { type: ['string', 'null'] },
|
|
77
|
+
action: { type: 'string', enum: ['hide', 'auto_resolve'] },
|
|
78
|
+
autoResolveDays: { type: ['integer', 'null'], minimum: 1, maximum: 365 },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
const ruleIdParamSchema = {
|
|
83
|
+
params: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
required: ['id'],
|
|
86
|
+
properties: { id: { type: 'integer' } },
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
// Bot-triage platform routes (CORE, always registered). Detection/override, ROI analytics,
|
|
90
|
+
// per-PR cross-bot dedup, and the mute / auto-triage rule store. Every handler is account-
|
|
91
|
+
// scoped via accountIdOf(req); id-addressed reads/writes verify ownership → 404. No AI.
|
|
92
|
+
export async function botTriageRoutes(app) {
|
|
93
|
+
// Every distinct reviewer in the account (human + automated), classified, with 90-day
|
|
94
|
+
// volume + a sample body — powers the Settings "Review bots" detected-reviewers table.
|
|
95
|
+
app.get('/api/bot-reviewers', async (req) => {
|
|
96
|
+
const resp = await listDetectedReviewers(accountIdOf(req));
|
|
97
|
+
return resp;
|
|
98
|
+
});
|
|
99
|
+
// Two-way manual override: force a reviewer automated (with kind/label) or back to human.
|
|
100
|
+
// Upserts a source='manual' classification the auto resolver never overwrites. 404 when the
|
|
101
|
+
// user id is unknown to this account's synced data.
|
|
102
|
+
app.patch('/api/bot-reviewers/:userId', { schema: overrideSchema }, async (req, reply) => {
|
|
103
|
+
const { userId } = req.params;
|
|
104
|
+
const body = req.body;
|
|
105
|
+
const classification = await setReviewerOverride(accountIdOf(req), userId, body);
|
|
106
|
+
if (!classification) {
|
|
107
|
+
reply.status(404);
|
|
108
|
+
return { error: 'NotFound', message: `Reviewer ${userId} not found` };
|
|
109
|
+
}
|
|
110
|
+
return classification;
|
|
111
|
+
});
|
|
112
|
+
// Bot ROI / utilisation over a window (default rolling_14): per-vendor threads/acted-on/
|
|
113
|
+
// untouched + verdict + trend + deterministic tuning suggestions. Cost fields are null here
|
|
114
|
+
// (the client overlays per-vendor cost from Pro settings).
|
|
115
|
+
app.get('/api/bot-analytics', { schema: analyticsSchema }, async (req) => {
|
|
116
|
+
const { window } = req.query;
|
|
117
|
+
const resp = await getBotAnalytics(accountIdOf(req), window);
|
|
118
|
+
return resp;
|
|
119
|
+
});
|
|
120
|
+
// The per-vendor PR drill-down behind one Bot-ROI row: the PRs that automated reviewer kind
|
|
121
|
+
// touched in the window (threads/comments/acted-on/untouched/bot-only), newest-activity first.
|
|
122
|
+
app.get('/api/bot-analytics/:kind/prs', { schema: vendorPrsSchema }, async (req) => {
|
|
123
|
+
const { kind } = req.params;
|
|
124
|
+
const { window } = req.query;
|
|
125
|
+
const resp = await getBotVendorPrs(accountIdOf(req), kind, window);
|
|
126
|
+
return resp;
|
|
127
|
+
});
|
|
128
|
+
// Cross-bot dedup clusters for one PR (≥2 automated reviewers on the same path/line window).
|
|
129
|
+
// Ownership-scoped → 404 for a PR this account doesn't own.
|
|
130
|
+
app.get('/api/prs/:id/bot-dedup', { schema: prIdParamSchema }, async (req, reply) => {
|
|
131
|
+
const { id } = req.params;
|
|
132
|
+
const resp = await getBotDedupClusters(id, accountIdOf(req));
|
|
133
|
+
if (!resp) {
|
|
134
|
+
reply.status(404);
|
|
135
|
+
return { error: 'NotFound', message: `PR ${id} not found` };
|
|
136
|
+
}
|
|
137
|
+
return resp;
|
|
138
|
+
});
|
|
139
|
+
// The account's mute / auto-triage rules.
|
|
140
|
+
app.get('/api/bot-mute-rules', async (req) => {
|
|
141
|
+
const rules = await listBotMuteRules(accountIdOf(req));
|
|
142
|
+
const resp = { rules };
|
|
143
|
+
return resp;
|
|
144
|
+
});
|
|
145
|
+
app.post('/api/bot-mute-rules', { schema: muteRuleSchema }, async (req) => {
|
|
146
|
+
const input = req.body;
|
|
147
|
+
const rule = await addBotMuteRule(accountIdOf(req), input);
|
|
148
|
+
return rule;
|
|
149
|
+
});
|
|
150
|
+
app.delete('/api/bot-mute-rules/:id', { schema: ruleIdParamSchema }, async (req, reply) => {
|
|
151
|
+
const { id } = req.params;
|
|
152
|
+
const ok = await deleteBotMuteRule(accountIdOf(req), id);
|
|
153
|
+
if (!ok) {
|
|
154
|
+
reply.status(404);
|
|
155
|
+
return { error: 'NotFound', message: `Mute rule ${id} not found` };
|
|
156
|
+
}
|
|
157
|
+
reply.status(204);
|
|
158
|
+
return null;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=bot-triage.js.map
|
package/dist/api/routes/prs.js
CHANGED
|
@@ -2,11 +2,12 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { config } from '../../config.js';
|
|
3
3
|
import { getAccessToken, getAccountUserId } from '../../auth/account.js';
|
|
4
4
|
import { fetchActionsJobLog } from '../../github/actions-logs.js';
|
|
5
|
-
import { getMentionCandidates, getPrDetail, getPrFilesContext, getPrWriteContext, getReviewerLogins, getUsersByLogins, markAllViewed, markPrMergedLocally, markPrViewed, upsertLocalPrComment, upsertLocalReview, } from '../../db/queries.js';
|
|
5
|
+
import { getMentionCandidates, getPrDetail, getPrFilesContext, getPrWriteContext, getReviewerLogins, getResolvableBotThreads, getUsersByLogins, markAllViewed, markPrMergedLocally, markPrViewed, upsertLocalPrComment, upsertLocalReview, } from '../../db/queries.js';
|
|
6
6
|
import { getCodeownersMatch } from '../../github/codeowners.js';
|
|
7
7
|
import { buildFileAnchors, fallbackAnchor, isFindingAnchored, } from '../../github/diff-anchor.js';
|
|
8
8
|
import { addIssueComment, fetchHeadShaFor, fetchMergeability, fetchPrFilesWithPatch, fetchPrHeadInfo, fetchRepoMergeConfig, mergePullRequest, postInlineComment, requestReviewers, rerunWorkflowRun, submitPrReview, updatePullRequestBranch, } from '../../github/mutations.js';
|
|
9
9
|
import { hydratePrDetail } from '../../sync/hydrate-detail.js';
|
|
10
|
+
import { resolveThreadsOnGitHub } from '../../bot-triage/resolve.js';
|
|
10
11
|
import { accountIdOf } from '../plugins/auth.js';
|
|
11
12
|
// GitHub anchors a file in the PR "Files changed" diff by the SHA-256 of its
|
|
12
13
|
// path (matches db/queries.ts + hydrate-detail.ts's diffAnchorId).
|
|
@@ -94,6 +95,19 @@ const approveSchema = {
|
|
|
94
95
|
properties: { body: { type: 'string' } },
|
|
95
96
|
},
|
|
96
97
|
};
|
|
98
|
+
const resolveBotThreadsSchema = {
|
|
99
|
+
...idParamSchema,
|
|
100
|
+
body: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
required: ['threadIds'],
|
|
103
|
+
additionalProperties: false,
|
|
104
|
+
properties: {
|
|
105
|
+
// minItems: 1 rejects `{threadIds: []}` with a 400 — a destructive endpoint should
|
|
106
|
+
// never be invoked with an empty selection (defence-in-depth with getResolvableBotThreads).
|
|
107
|
+
threadIds: { type: 'array', items: { type: 'integer' }, minItems: 1, maxItems: 200 },
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
};
|
|
97
111
|
const mergeSchema = {
|
|
98
112
|
...idParamSchema,
|
|
99
113
|
body: {
|
|
@@ -276,6 +290,25 @@ export async function prRoutes(app) {
|
|
|
276
290
|
}
|
|
277
291
|
return { status: 'ok' };
|
|
278
292
|
});
|
|
293
|
+
// Bulk-resolve the review-BOT threads a later commit has likely addressed — Pierre's
|
|
294
|
+
// "clear the bot backlog in one click." NEVER automatic: the client sends the explicit
|
|
295
|
+
// reviewed list of thread ids; the server RE-DERIVES the eligible set (owned + bot-
|
|
296
|
+
// originated + `likely_addressed`), intersects it with that list, and resolves only that
|
|
297
|
+
// — so a stale client can never resolve a thread the server wouldn't itself offer. Each
|
|
298
|
+
// thread is resolved on GitHub + locally stamped; per-thread failures are reported, not fatal.
|
|
299
|
+
app.post('/api/prs/:id/resolve-bot-threads', { schema: resolveBotThreadsSchema }, async (req) => {
|
|
300
|
+
const { id } = req.params;
|
|
301
|
+
const { threadIds } = req.body;
|
|
302
|
+
const accountId = accountIdOf(req);
|
|
303
|
+
// Server RE-DERIVES the eligible set (owned + automated-reviewer-originated +
|
|
304
|
+
// `likely_addressed` + unresolved) ∩ the client's reviewed list, then resolves each via
|
|
305
|
+
// the shared helper (the SAME code path the standing auto-triage job uses). An empty
|
|
306
|
+
// eligible set — PR not owned / no such threads / a fully-stale client list — is a no-op,
|
|
307
|
+
// not an error (the helper short-circuits before any token fetch). Status stays 200 even
|
|
308
|
+
// on partial failure; the body carries per-thread outcomes.
|
|
309
|
+
const eligible = await getResolvableBotThreads(id, accountId, threadIds);
|
|
310
|
+
return resolveThreadsOnGitHub(accountId, eligible);
|
|
311
|
+
});
|
|
279
312
|
// Post a new issue-level (general) PR comment, then optimistically stamp it
|
|
280
313
|
// locally so it shows before the next sync.
|
|
281
314
|
app.post('/api/prs/:id/comment', { schema: commentSchema }, async (req, reply) => {
|
package/dist/app.js
CHANGED
|
@@ -20,6 +20,7 @@ import { mergersRoutes } from './api/routes/mergers.js';
|
|
|
20
20
|
import { insightsRoutes } from './api/routes/insights.js';
|
|
21
21
|
import { activityRoutes } from './api/routes/activity.js';
|
|
22
22
|
import { billingRoutes } from './api/routes/billing.js';
|
|
23
|
+
import { botTriageRoutes } from './api/routes/bot-triage.js';
|
|
23
24
|
export async function buildApp() {
|
|
24
25
|
const app = Fastify({
|
|
25
26
|
// In production (the installed CLI) the per-request "incoming request" /
|
|
@@ -132,6 +133,9 @@ export async function buildApp() {
|
|
|
132
133
|
await app.register(mergersRoutes);
|
|
133
134
|
await app.register(insightsRoutes);
|
|
134
135
|
await app.register(activityRoutes);
|
|
136
|
+
// Bot-triage platform (CORE, always registered): detection/override, ROI analytics,
|
|
137
|
+
// cross-bot dedup, mute / auto-triage rules. Account-scoped; no AI.
|
|
138
|
+
await app.register(botTriageRoutes);
|
|
135
139
|
// Stripe billing seam (checkout redirect + webhook). Registered in both modes;
|
|
136
140
|
// inert until the STRIPE_* env vars are set (webhook 501s unconfigured).
|
|
137
141
|
await app.register(billingRoutes);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { config } from '../config.js';
|
|
3
|
+
import { db, schema } from '../db/client.js';
|
|
4
|
+
import { getAutoResolveCandidates, getResolvableBotThreads } from '../db/queries.js';
|
|
5
|
+
import { resolveThreadsOnGitHub } from './resolve.js';
|
|
6
|
+
const { botMuteRules } = schema;
|
|
7
|
+
// Accounts that have at least one standing `auto_resolve` rule. The sweep only touches
|
|
8
|
+
// these — with zero rules the whole feature is inert (this returns []). Distinct, portable.
|
|
9
|
+
async function accountsWithAutoResolveRules() {
|
|
10
|
+
const rows = await db
|
|
11
|
+
.select({ accountId: botMuteRules.accountId })
|
|
12
|
+
.from(botMuteRules)
|
|
13
|
+
.where(eq(botMuteRules.action, 'auto_resolve'))
|
|
14
|
+
.groupBy(botMuteRules.accountId)
|
|
15
|
+
.execute();
|
|
16
|
+
return rows.map((r) => r.accountId);
|
|
17
|
+
}
|
|
18
|
+
// Standing bot auto-triage sweep (CORE, deterministic, rule-gated). Registered on a cron by
|
|
19
|
+
// the scheduler; runs local + cloud. For each account with an `auto_resolve` rule it asks the
|
|
20
|
+
// engine for the candidate threads (rule-gated by vendor/path/severity AND age > rule.days,
|
|
21
|
+
// only `likely_addressed` + unresolved), RE-DERIVES eligibility exactly as the manual route
|
|
22
|
+
// does (`getResolvableBotThreads` → owned + automated-originated + `likely_addressed` +
|
|
23
|
+
// unresolved + node id, mapped to node ids), then resolves each via the SHARED helper.
|
|
24
|
+
//
|
|
25
|
+
// Guardrails (all load-bearing): opt-in (inert with zero rules); only `likely_addressed`;
|
|
26
|
+
// age-gated; NEVER a merge (resolve only); a structured pino line per resolved thread; errors
|
|
27
|
+
// caught per-account so one bad token doesn't abort the sweep, and per-thread inside the
|
|
28
|
+
// shared helper so one bad thread doesn't abort the account. Undoable = unresolve on GitHub
|
|
29
|
+
// (no local hard state beyond the derivedState the next sync recomputes).
|
|
30
|
+
export async function runAutoTriageSweep(log) {
|
|
31
|
+
// Defensive: the scheduler only schedules this when scheduling is enabled, but honour the
|
|
32
|
+
// gate directly too so a stray caller can never bypass it.
|
|
33
|
+
if (config.disableScheduler)
|
|
34
|
+
return;
|
|
35
|
+
const accountIds = await accountsWithAutoResolveRules();
|
|
36
|
+
if (accountIds.length === 0)
|
|
37
|
+
return;
|
|
38
|
+
for (const accountId of accountIds) {
|
|
39
|
+
try {
|
|
40
|
+
const candidates = await getAutoResolveCandidates(accountId);
|
|
41
|
+
for (const { prId, threadIds } of candidates) {
|
|
42
|
+
// Re-derive from scratch — the candidate set is rule/age-gated; this maps thread ids to
|
|
43
|
+
// GitHub node ids AND re-confirms the (owned + automated + likely_addressed + unresolved)
|
|
44
|
+
// predicate, so a thread whose state changed since the candidate query is dropped.
|
|
45
|
+
const eligible = await getResolvableBotThreads(prId, accountId, threadIds);
|
|
46
|
+
if (eligible.length === 0)
|
|
47
|
+
continue;
|
|
48
|
+
const result = await resolveThreadsOnGitHub(accountId, eligible);
|
|
49
|
+
for (const r of result.results) {
|
|
50
|
+
if (r.ok)
|
|
51
|
+
log.info({ accountId, prId, threadId: r.threadId }, 'bot auto-resolved');
|
|
52
|
+
else
|
|
53
|
+
log.warn({ accountId, prId, threadId: r.threadId }, 'bot auto-resolve failed');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
// One account's failure (e.g. an expired/absent token) must not abort the others.
|
|
59
|
+
log.error({ err, accountId }, 'bot auto-triage sweep failed for account');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=auto-triage.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getAccessToken } from '../auth/account.js';
|
|
2
|
+
import { setReviewThreadResolved } from '../github/mutations.js';
|
|
3
|
+
import { stampThreadResolved } from '../db/queries.js';
|
|
4
|
+
// Resolve a batch of already-eligible review threads on GitHub AND stamp them resolved
|
|
5
|
+
// locally. This is the ONE place the GitHub `resolveReviewThread` mutation is driven for
|
|
6
|
+
// bot-thread triage — shared verbatim by BOTH the manual `POST /api/prs/:id/resolve-bot-
|
|
7
|
+
// threads` route and the standing auto-triage scheduled job, so their behaviour can't drift.
|
|
8
|
+
//
|
|
9
|
+
// Callers own eligibility: both `getResolvableBotThreads` and `getAutoResolveCandidates`
|
|
10
|
+
// return ONLY owned + automated-reviewer-originated + `likely_addressed` + unresolved threads
|
|
11
|
+
// (with a GitHub node id), so this helper never has to re-derive it. It NEVER merges — it only
|
|
12
|
+
// resolves. Sequential (not Promise.all) to stay gentle on the GraphQL rate limit (a bot
|
|
13
|
+
// backlog is a handful of threads); per-thread try/catch so one bad thread or a moved head
|
|
14
|
+
// never aborts the batch. The token is fetched once per batch from the OWNING account, so a
|
|
15
|
+
// bad/absent token throws BEFORE the loop — the caller (route or job) decides how to surface it.
|
|
16
|
+
export async function resolveThreadsOnGitHub(accountId, threads) {
|
|
17
|
+
const out = { resolved: 0, failed: 0, results: [] };
|
|
18
|
+
// Empty selection → a no-op (never even fetch a token) — matches the manual route's
|
|
19
|
+
// "stale client list / no eligible threads is a no-op, not an error" contract.
|
|
20
|
+
if (threads.length === 0)
|
|
21
|
+
return out;
|
|
22
|
+
const token = await getAccessToken(accountId);
|
|
23
|
+
for (const t of threads) {
|
|
24
|
+
try {
|
|
25
|
+
await setReviewThreadResolved(token, t.threadNodeId, true);
|
|
26
|
+
const derivedState = await stampThreadResolved(t.id, true, accountId);
|
|
27
|
+
out.results.push({ threadId: t.id, ok: true, derivedState: derivedState ?? 'resolved' });
|
|
28
|
+
out.resolved += 1;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
out.results.push({ threadId: t.id, ok: false, derivedState: null });
|
|
32
|
+
out.failed += 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
-- Bot-Triage Platform (WS1): capture the GitHub GraphQL __typename of an actor
|
|
2
|
+
-- ('Bot' | 'User' | 'Organization' | …) on the global users table. Plain text (no
|
|
3
|
+
-- enum — the __typename set varies), nullable. A 'Bot' typename is a hard
|
|
4
|
+
-- automated-reviewer signal for the classifier. users stays GLOBAL (no account_id).
|
|
5
|
+
-- The Postgres baseline is regenerated separately via `pnpm db:generate:pg`.
|
|
6
|
+
ALTER TABLE `users` ADD `github_type` text;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- Bot-Triage Platform (WS1): account-scoped classification cache for automated
|
|
2
|
+
-- reviewers. One row per (account, author); AUTO rows written by the layered resolver,
|
|
3
|
+
-- MANUAL rows (source='manual') by the override route and never overwritten by auto.
|
|
4
|
+
-- Merged with the global vendor login map on read. Account-scoped isolation.
|
|
5
|
+
-- The Postgres baseline is regenerated separately via `pnpm db:generate:pg`.
|
|
6
|
+
CREATE TABLE `bot_review_classification` (
|
|
7
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
8
|
+
`account_id` integer NOT NULL,
|
|
9
|
+
`author_user_id` integer NOT NULL,
|
|
10
|
+
`automated` integer NOT NULL,
|
|
11
|
+
`kind` text,
|
|
12
|
+
`label` text,
|
|
13
|
+
`confidence` text NOT NULL,
|
|
14
|
+
`source` text NOT NULL,
|
|
15
|
+
`reasons_json` text,
|
|
16
|
+
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
|
|
17
|
+
FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON UPDATE no action ON DELETE no action,
|
|
18
|
+
FOREIGN KEY (`author_user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
|
|
19
|
+
);
|
|
20
|
+
--> statement-breakpoint
|
|
21
|
+
CREATE UNIQUE INDEX `brc_account_author` ON `bot_review_classification` (`account_id`,`author_user_id`);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- Bot-Triage Platform (WS6): account-scoped mute / auto-triage rules. A rule matches
|
|
2
|
+
-- automated-reviewer threads by vendor_kind × path_glob × severity (null = any) and
|
|
3
|
+
-- either 'hide's them from the noise counts/feed or (with auto_resolve_days) marks
|
|
4
|
+
-- likely_addressed threads older than N days for the standing auto-resolve job.
|
|
5
|
+
-- Account-scoped isolation. The Postgres baseline is regenerated separately via
|
|
6
|
+
-- `pnpm db:generate:pg`.
|
|
7
|
+
CREATE TABLE `bot_mute_rules` (
|
|
8
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
9
|
+
`account_id` integer NOT NULL,
|
|
10
|
+
`vendor_kind` text,
|
|
11
|
+
`path_glob` text,
|
|
12
|
+
`severity` text,
|
|
13
|
+
`action` text NOT NULL,
|
|
14
|
+
`auto_resolve_days` integer,
|
|
15
|
+
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
|
16
|
+
FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON UPDATE no action ON DELETE no action
|
|
17
|
+
);
|
|
18
|
+
--> statement-breakpoint
|
|
19
|
+
CREATE INDEX `bmr_account_idx` ON `bot_mute_rules` (`account_id`);
|
|
@@ -190,6 +190,27 @@
|
|
|
190
190
|
"when": 1783382400000,
|
|
191
191
|
"tag": "0026_ai_credit_allowance",
|
|
192
192
|
"breakpoints": true
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"idx": 27,
|
|
196
|
+
"version": "6",
|
|
197
|
+
"when": 1783820880000,
|
|
198
|
+
"tag": "0027_user_github_type",
|
|
199
|
+
"breakpoints": true
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"idx": 28,
|
|
203
|
+
"version": "6",
|
|
204
|
+
"when": 1783820880001,
|
|
205
|
+
"tag": "0028_bot_review_classification",
|
|
206
|
+
"breakpoints": true
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"idx": 29,
|
|
210
|
+
"version": "6",
|
|
211
|
+
"when": 1783820880002,
|
|
212
|
+
"tag": "0029_bot_mute_rules",
|
|
213
|
+
"breakpoints": true
|
|
193
214
|
}
|
|
194
215
|
]
|
|
195
216
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
CREATE TABLE "bot_mute_rules" (
|
|
2
|
+
"id" serial PRIMARY KEY NOT NULL,
|
|
3
|
+
"account_id" integer NOT NULL,
|
|
4
|
+
"vendor_kind" text,
|
|
5
|
+
"path_glob" text,
|
|
6
|
+
"severity" text,
|
|
7
|
+
"action" text NOT NULL,
|
|
8
|
+
"auto_resolve_days" integer,
|
|
9
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
10
|
+
);
|
|
11
|
+
--> statement-breakpoint
|
|
12
|
+
CREATE TABLE "bot_review_classification" (
|
|
13
|
+
"id" serial PRIMARY KEY NOT NULL,
|
|
14
|
+
"account_id" integer NOT NULL,
|
|
15
|
+
"author_user_id" integer NOT NULL,
|
|
16
|
+
"automated" boolean NOT NULL,
|
|
17
|
+
"kind" text,
|
|
18
|
+
"label" text,
|
|
19
|
+
"confidence" text NOT NULL,
|
|
20
|
+
"source" text NOT NULL,
|
|
21
|
+
"reasons_json" jsonb,
|
|
22
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
23
|
+
);
|
|
24
|
+
--> statement-breakpoint
|
|
25
|
+
ALTER TABLE "users" ADD COLUMN "github_type" text;--> statement-breakpoint
|
|
26
|
+
ALTER TABLE "bot_mute_rules" ADD CONSTRAINT "bot_mute_rules_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
27
|
+
ALTER TABLE "bot_review_classification" ADD CONSTRAINT "bot_review_classification_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
28
|
+
ALTER TABLE "bot_review_classification" ADD CONSTRAINT "bot_review_classification_author_user_id_users_id_fk" FOREIGN KEY ("author_user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
29
|
+
CREATE INDEX "bmr_account_idx" ON "bot_mute_rules" USING btree ("account_id");--> statement-breakpoint
|
|
30
|
+
CREATE UNIQUE INDEX "brc_account_author" ON "bot_review_classification" USING btree ("account_id","author_user_id");
|