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.
Files changed (58) hide show
  1. package/dist/api/routes/bot-triage.js +161 -0
  2. package/dist/api/routes/prs.js +34 -1
  3. package/dist/app.js +4 -0
  4. package/dist/bot-triage/auto-triage.js +63 -0
  5. package/dist/bot-triage/resolve.js +37 -0
  6. package/dist/db/migrations/0027_user_github_type.sql +6 -0
  7. package/dist/db/migrations/0028_bot_review_classification.sql +21 -0
  8. package/dist/db/migrations/0029_bot_mute_rules.sql +19 -0
  9. package/dist/db/migrations/meta/_journal.json +21 -0
  10. package/dist/db/migrations-pg/0016_cute_ghost_rider.sql +30 -0
  11. package/dist/db/migrations-pg/meta/0016_snapshot.json +2774 -0
  12. package/dist/db/migrations-pg/meta/_journal.json +7 -0
  13. package/dist/db/queries.js +1419 -13
  14. package/dist/db/schema.pg.js +45 -0
  15. package/dist/db/schema.sqlite.js +51 -0
  16. package/dist/github/queries.js +20 -0
  17. package/dist/review/post-seam.js +8 -2
  18. package/dist/sync/app-attribution.js +29 -0
  19. package/dist/sync/bot-detection.js +91 -2
  20. package/dist/sync/review-fingerprint.js +112 -0
  21. package/dist/sync/reviewer-behavior.js +103 -0
  22. package/dist/sync/reviewer-classify.js +275 -0
  23. package/dist/sync/scheduler.js +20 -0
  24. package/dist/sync/upsert.js +54 -1
  25. package/package.json +1 -1
  26. package/public/assets/{AiFixTab-B23-MMEW.js → AiFixTab-D_ACiODs.js} +1 -1
  27. package/public/assets/{ClaudeReviewTab-y5uOQ5r8.js → ClaudeReviewTab-DidHqzvf.js} +1 -1
  28. package/public/assets/index-BlRvtQJd.js +53 -0
  29. package/public/assets/index-X4Ro8Jjo.css +1 -0
  30. package/public/index.html +2 -2
  31. package/public-landing/assets/{index-DoT9NPaW.js → index-C8_MOIpM.js} +9 -9
  32. package/public-landing/assets/index-Lj-kzO3U.css +1 -0
  33. package/public-landing/index.html +8 -8
  34. package/public-landing/og-image.png +0 -0
  35. package/public-landing/shots/activity-feed-pro.png +0 -0
  36. package/public-landing/shots/activity-feed.png +0 -0
  37. package/public-landing/shots/bot-dedup.png +0 -0
  38. package/public-landing/shots/bot-inhouse.png +0 -0
  39. package/public-landing/shots/bot-only-review.png +0 -0
  40. package/public-landing/shots/bot-review.png +0 -0
  41. package/public-landing/shots/bot-roi.png +0 -0
  42. package/public-landing/shots/bot-settings.png +0 -0
  43. package/public-landing/shots/claude-review.png +0 -0
  44. package/public-landing/shots/flow-metrics.png +0 -0
  45. package/public-landing/shots/flow-review-2-memory.png +0 -0
  46. package/public-landing/shots/flow-review-3-findings.png +0 -0
  47. package/public-landing/shots/flow-review-4-post.png +0 -0
  48. package/public-landing/shots/insights.png +0 -0
  49. package/public-landing/shots/open-pr-strip.png +0 -0
  50. package/public-landing/shots/pr-detail.png +0 -0
  51. package/public-landing/shots/repo-console-free.png +0 -0
  52. package/public-landing/shots/repo-console.png +0 -0
  53. package/public-landing/shots/settings.png +0 -0
  54. package/public-landing/shots/sprint-report.png +0 -0
  55. package/public-landing/shots/timeline.png +0 -0
  56. package/public/assets/index-BplNgl1k.js +0 -49
  57. package/public/assets/index-DKZfvKB1.css +0 -1
  58. 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
@@ -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");