fix: add defensive checks; improve code quality

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I8c88c07bd852c78d196705da03f372e06a6a6964
This commit is contained in:
raf 2026-02-07 16:13:14 +03:00
commit 38105ec09c
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
4 changed files with 255 additions and 112 deletions

View file

@ -1,12 +1,10 @@
import fs from 'node:fs'; import fs from "node:fs";
import path from 'node:path'; import path from "node:path";
import { createJiti } from 'jiti'; import dotenv from "dotenv";
import dotenv from 'dotenv'; import type { Config } from "./types.js";
import type { Config } from './types.js';
dotenv.config(); // Suppress dotenv warnings
dotenv.config({ quiet: true });
const jiti = createJiti(__filename, { interopDefault: true });
const defaults: Config = { const defaults: Config = {
server: { port: 3000 }, server: { port: 3000 },
@ -34,31 +32,34 @@ const defaults: Config = {
includeReasoning: true, includeReasoning: true,
messages: { messages: {
positive: [ positive: [
'This {type} looks great for the trout! All signals point upstream.', "This {type} looks great for the trout! All signals point upstream.",
'The trout approve of this {type}. Swim on!', "The trout approve of this {type}. Swim on!",
'Splashing good news - this {type} is looking healthy.', "Splashing good news - this {type} is looking healthy.",
], ],
negative: [ negative: [
'This {type} is muddying the waters. The trout are concerned.', "This {type} is muddying the waters. The trout are concerned.",
'Warning: the trout sense trouble in this {type}.', "Warning: the trout sense trouble in this {type}.",
'Something smells fishy about this {type}. Please review.', "Something smells fishy about this {type}. Please review.",
], ],
neutral: [ neutral: [
'The trout have no strong feelings about this {type}.', "The trout have no strong feelings about this {type}.",
'This {type} is neither upstream nor downstream. Neutral waters.', "This {type} is neither upstream nor downstream. Neutral waters.",
'The trout are watching this {type} with mild interest.', "The trout are watching this {type} with mild interest.",
], ],
}, },
commentMarker: '<!-- troutbot -->', commentMarker: "<!-- troutbot -->",
allowUpdates: false, allowUpdates: false,
}, },
logging: { logging: {
level: 'info', level: "info",
file: 'troutbot.log', file: "troutbot.log",
}, },
}; };
export function deepMerge<T extends Record<string, unknown>>(target: T, source: Partial<T>): T { export function deepMerge<T extends Record<string, unknown>>(
target: T,
source: Partial<T>,
): T {
const result = { ...target }; const result = { ...target };
for (const key of Object.keys(source) as (keyof T)[]) { for (const key of Object.keys(source) as (keyof T)[]) {
const sourceVal = source[key]; const sourceVal = source[key];
@ -66,15 +67,15 @@ export function deepMerge<T extends Record<string, unknown>>(target: T, source:
if ( if (
sourceVal !== null && sourceVal !== null &&
sourceVal !== undefined && sourceVal !== undefined &&
typeof sourceVal === 'object' && typeof sourceVal === "object" &&
!Array.isArray(sourceVal) && !Array.isArray(sourceVal) &&
typeof targetVal === 'object' && typeof targetVal === "object" &&
!Array.isArray(targetVal) && !Array.isArray(targetVal) &&
targetVal !== null targetVal !== null
) { ) {
result[key] = deepMerge( result[key] = deepMerge(
targetVal as Record<string, unknown>, targetVal as Record<string, unknown>,
sourceVal as Record<string, unknown> sourceVal as Record<string, unknown>,
) as T[keyof T]; ) as T[keyof T];
} else if (sourceVal !== undefined) { } else if (sourceVal !== undefined) {
result[key] = sourceVal as T[keyof T]; result[key] = sourceVal as T[keyof T];
@ -83,30 +84,35 @@ export function deepMerge<T extends Record<string, unknown>>(target: T, source:
return result; return result;
} }
export function loadConfig(): Config { export async function loadConfig(): Promise<Config> {
const configPath = process.env.CONFIG_PATH || 'config.ts'; const configPath = process.env.CONFIG_PATH || "config.ts";
const resolvedPath = path.resolve(configPath); const resolvedPath = path.resolve(configPath);
let fileConfig: Partial<Config> = {}; let fileConfig: Partial<Config> = {};
if (fs.existsSync(resolvedPath)) { if (fs.existsSync(resolvedPath)) {
const loaded = jiti(resolvedPath) as Partial<Config> | { default: Partial<Config> }; const loaded = await import(resolvedPath);
fileConfig = 'default' in loaded ? loaded.default : loaded; fileConfig =
"default" in loaded
? loaded.default
: (loaded as unknown as Partial<Config>);
} else if (process.env.CONFIG_PATH) { } else if (process.env.CONFIG_PATH) {
console.warn( console.warn(
`Warning: CONFIG_PATH is set to "${process.env.CONFIG_PATH}" but file not found at ${resolvedPath}` `Warning: CONFIG_PATH is set to "${process.env.CONFIG_PATH}" but file not found at ${resolvedPath}`,
); );
} }
const config = deepMerge( const config = deepMerge(
defaults as unknown as Record<string, unknown>, defaults as unknown as Record<string, unknown>,
fileConfig as unknown as Record<string, unknown> fileConfig as unknown as Record<string, unknown>,
) as unknown as Config; ) as unknown as Config;
// Environment variable overrides // Environment variable overrides
if (process.env.PORT) { if (process.env.PORT) {
const parsed = parseInt(process.env.PORT, 10); const parsed = parseInt(process.env.PORT, 10);
if (Number.isNaN(parsed)) { if (Number.isNaN(parsed)) {
throw new Error(`Invalid PORT value: "${process.env.PORT}" is not a number`); throw new Error(
`Invalid PORT value: "${process.env.PORT}" is not a number`,
);
} }
config.server.port = parsed; config.server.port = parsed;
} }
@ -119,7 +125,7 @@ export function loadConfig(): Config {
config.dashboard = { config.dashboard = {
...(config.dashboard || { enabled: true }), ...(config.dashboard || { enabled: true }),
auth: { auth: {
type: 'token', type: "token",
token: process.env.DASHBOARD_TOKEN, token: process.env.DASHBOARD_TOKEN,
}, },
}; };
@ -129,18 +135,18 @@ export function loadConfig(): Config {
config.dashboard = { config.dashboard = {
...(config.dashboard || { enabled: true }), ...(config.dashboard || { enabled: true }),
auth: { auth: {
type: 'basic', type: "basic",
username: process.env.DASHBOARD_USERNAME, username: process.env.DASHBOARD_USERNAME,
password: process.env.DASHBOARD_PASSWORD, password: process.env.DASHBOARD_PASSWORD,
}, },
}; };
} }
const validLogLevels = ['debug', 'info', 'warn', 'error']; const validLogLevels = ["debug", "info", "warn", "error"];
if (process.env.LOG_LEVEL) { if (process.env.LOG_LEVEL) {
if (!validLogLevels.includes(process.env.LOG_LEVEL)) { if (!validLogLevels.includes(process.env.LOG_LEVEL)) {
throw new Error( throw new Error(
`Invalid LOG_LEVEL: "${process.env.LOG_LEVEL}". Must be one of: ${validLogLevels.join(', ')}` `Invalid LOG_LEVEL: "${process.env.LOG_LEVEL}". Must be one of: ${validLogLevels.join(", ")}`,
); );
} }
config.logging.level = process.env.LOG_LEVEL; config.logging.level = process.env.LOG_LEVEL;
@ -151,23 +157,36 @@ export function loadConfig(): Config {
} }
export function validate(config: Config): void { export function validate(config: Config): void {
if (!config.server.port || config.server.port < 1 || config.server.port > 65535) { if (
throw new Error('Invalid server port'); !config.server.port ||
config.server.port < 1 ||
config.server.port > 65535
) {
throw new Error("Invalid server port");
} }
const { backends } = config.engine; const { backends } = config.engine;
if (!backends.checks.enabled && !backends.diff.enabled && !backends.quality.enabled) { if (
throw new Error('At least one engine backend must be enabled'); !backends.checks.enabled &&
!backends.diff.enabled &&
!backends.quality.enabled
) {
throw new Error("At least one engine backend must be enabled");
} }
const { weights } = config.engine; const { weights } = config.engine;
for (const [key, value] of Object.entries(weights)) { for (const [key, value] of Object.entries(weights)) {
if (value < 0) { if (value < 0) {
throw new Error(`Backend weight "${key}" must be non-negative, got ${value}`); throw new Error(
`Backend weight "${key}" must be non-negative, got ${value}`,
);
} }
} }
if (config.engine.confidenceThreshold < 0 || config.engine.confidenceThreshold > 1) { if (
throw new Error('confidenceThreshold must be between 0 and 1'); config.engine.confidenceThreshold < 0 ||
config.engine.confidenceThreshold > 1
) {
throw new Error("confidenceThreshold must be between 0 and 1");
} }
} }

View file

@ -1,41 +1,53 @@
import type { FiltersConfig, WebhookEvent } from './types.js'; import type { FiltersConfig, WebhookEvent } from "./types.js";
export function shouldProcess( export function shouldProcess(
event: WebhookEvent, event: WebhookEvent,
filters: FiltersConfig filters: FiltersConfig,
): { pass: boolean; reason?: string } { ): { pass: boolean; reason?: string } {
// Label filters // Label filters
if (filters.labels.include.length > 0) { if (filters.labels.include.length > 0) {
const hasRequired = event.labels.some((l) => filters.labels.include.includes(l)); const hasRequired = event.labels.some((l) =>
filters.labels.include.includes(l),
);
if (!hasRequired) { if (!hasRequired) {
return { pass: false, reason: 'Missing required label' }; return { pass: false, reason: "Missing required label" };
} }
} }
if (filters.labels.exclude.length > 0) { if (filters.labels.exclude.length > 0) {
const hasExcluded = event.labels.some((l) => filters.labels.exclude.includes(l)); const hasExcluded = event.labels.some((l) =>
filters.labels.exclude.includes(l),
);
if (hasExcluded) { if (hasExcluded) {
return { pass: false, reason: 'Has excluded label' }; return { pass: false, reason: "Has excluded label" };
} }
} }
// Author filters // Author filters
if (filters.authors.include && filters.authors.include.length > 0) { if (filters.authors.include && filters.authors.include.length > 0) {
if (!filters.authors.include.includes(event.author)) { const normalizedAuthor = event.author.toLowerCase();
return { pass: false, reason: 'Author not in include list' }; const hasIncluded = filters.authors.include.some(
(a) => a.toLowerCase() === normalizedAuthor,
);
if (!hasIncluded) {
return { pass: false, reason: "Author not in include list" };
} }
} }
if (filters.authors.exclude.length > 0) { if (filters.authors.exclude.length > 0) {
if (filters.authors.exclude.includes(event.author)) { const normalizedAuthor = event.author.toLowerCase();
return { pass: false, reason: 'Author is excluded' }; const isExcluded = filters.authors.exclude.some(
(a) => a.toLowerCase() === normalizedAuthor,
);
if (isExcluded) {
return { pass: false, reason: "Author is excluded" };
} }
} }
// Branch filters (PRs only) // Branch filters (PRs only)
if (event.branch && filters.branches.include.length > 0) { if (event.branch && filters.branches.include.length > 0) {
if (!filters.branches.include.includes(event.branch)) { if (!filters.branches.include.includes(event.branch)) {
return { pass: false, reason: 'Branch not in include list' }; return { pass: false, reason: "Branch not in include list" };
} }
} }

View file

@ -1,12 +1,14 @@
import { Octokit } from '@octokit/rest'; import { Octokit } from "@octokit/rest";
import { getLogger } from './logger.js'; import { getLogger } from "./logger.js";
import type { CheckRun, PRFile, ResponseConfig } from './types.js'; import type { CheckRun, PRFile } from "./types.js";
let octokit: Octokit | null = null; let octokit: Octokit | null = null;
export function initGitHub(token?: string): void { export function initGitHub(token?: string): void {
if (!token) { if (!token) {
getLogger().warn('No GITHUB_TOKEN set - running in dry-run mode, comments will not be posted'); getLogger().warn(
"No GITHUB_TOKEN set - running in dry-run mode, comments will not be posted",
);
return; return;
} }
octokit = new Octokit({ auth: token }); octokit = new Octokit({ auth: token });
@ -21,13 +23,20 @@ export async function postComment(
owner: string, owner: string,
repo: string, repo: string,
issueNumber: number, issueNumber: number,
body: string body: string,
): Promise<void> { ): Promise<void> {
if (!octokit) { if (!octokit) {
getLogger().info(`[dry-run] Would post comment on ${owner}/${repo}#${issueNumber}:\n${body}`); getLogger().info(
`[dry-run] Would post comment on ${owner}/${repo}#${issueNumber}:\n${body}`,
);
return; return;
} }
await octokit.issues.createComment({ owner, repo, issue_number: issueNumber, body }); await octokit.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body,
});
getLogger().info(`Posted comment on ${owner}/${repo}#${issueNumber}`); getLogger().info(`Posted comment on ${owner}/${repo}#${issueNumber}`);
} }
@ -35,7 +44,7 @@ export async function hasExistingComment(
owner: string, owner: string,
repo: string, repo: string,
issueNumber: number, issueNumber: number,
marker: string marker: string,
): Promise<{ exists: boolean; commentId?: number }> { ): Promise<{ exists: boolean; commentId?: number }> {
if (!octokit) { if (!octokit) {
return { exists: false }; return { exists: false };
@ -59,13 +68,18 @@ export async function updateComment(
owner: string, owner: string,
repo: string, repo: string,
commentId: number, commentId: number,
body: string body: string,
): Promise<void> { ): Promise<void> {
if (!octokit) { if (!octokit) {
getLogger().info(`[dry-run] Would update comment ${commentId}:\n${body}`); getLogger().info(`[dry-run] Would update comment ${commentId}:\n${body}`);
return; return;
} }
await octokit.issues.updateComment({ owner, repo, comment_id: commentId, body }); await octokit.issues.updateComment({
owner,
repo,
comment_id: commentId,
body,
});
getLogger().info(`Updated comment ${commentId} on ${owner}/${repo}`); getLogger().info(`Updated comment ${commentId} on ${owner}/${repo}`);
} }
@ -74,26 +88,41 @@ export async function createReaction(
repo: string, repo: string,
commentId: number, commentId: number,
reaction: reaction:
| 'thumbs_up' | "thumbs_up"
| 'thumbs_down' | "thumbs_down"
| 'laugh' | "laugh"
| 'confused' | "confused"
| 'heart' | "heart"
| 'hooray' | "hooray"
| 'eyes' | "eyes"
| 'rocket' | "rocket",
): Promise<void> { ): Promise<void> {
if (!octokit) { if (!octokit) {
getLogger().info(`[dry-run] Would add ${reaction} reaction to comment ${commentId}`); getLogger().info(
`[dry-run] Would add ${reaction} reaction to comment ${commentId}`,
);
return; return;
} }
// Map thumbs_up/thumbs_down to GitHub API format (+1/-1) // Map thumbs_up/thumbs_down to GitHub API format (+1/-1)
const content = reaction === 'thumbs_up' ? '+1' : reaction === 'thumbs_down' ? '-1' : reaction; const content =
reaction === "thumbs_up"
? "+1"
: reaction === "thumbs_down"
? "-1"
: reaction;
await octokit.reactions.createForIssueComment({ await octokit.reactions.createForIssueComment({
owner, owner,
repo, repo,
comment_id: commentId, comment_id: commentId,
content: content as '+1' | '-1' | 'laugh' | 'confused' | 'heart' | 'hooray' | 'eyes' | 'rocket', content: content as
| "+1"
| "-1"
| "laugh"
| "confused"
| "heart"
| "hooray"
| "eyes"
| "rocket",
}); });
getLogger().info(`Added ${reaction} reaction to comment ${commentId}`); getLogger().info(`Added ${reaction} reaction to comment ${commentId}`);
} }
@ -102,10 +131,10 @@ export async function createReaction(
export async function fetchCheckRuns( export async function fetchCheckRuns(
owner: string, owner: string,
repo: string, repo: string,
ref: string ref: string,
): Promise<CheckRun[]> { ): Promise<CheckRun[]> {
if (!octokit) { if (!octokit) {
getLogger().debug('[dry-run] Cannot fetch check runs without a token'); getLogger().debug("[dry-run] Cannot fetch check runs without a token");
return []; return [];
} }
@ -126,10 +155,10 @@ export async function fetchCheckRuns(
export async function fetchPRFiles( export async function fetchPRFiles(
owner: string, owner: string,
repo: string, repo: string,
prNumber: number prNumber: number,
): Promise<PRFile[]> { ): Promise<PRFile[]> {
if (!octokit) { if (!octokit) {
getLogger().debug('[dry-run] Cannot fetch PR files without a token'); getLogger().debug("[dry-run] Cannot fetch PR files without a token");
return []; return [];
} }
@ -151,7 +180,7 @@ export async function fetchPRFiles(
export async function fetchPR( export async function fetchPR(
owner: string, owner: string,
repo: string, repo: string,
prNumber: number prNumber: number,
): Promise<{ ): Promise<{
title: string; title: string;
body: string; body: string;
@ -163,12 +192,18 @@ export async function fetchPR(
if (!octokit) return null; if (!octokit) return null;
try { try {
const { data } = await octokit.pulls.get({ owner, repo, pull_number: prNumber }); const { data } = await octokit.pulls.get({
owner,
repo,
pull_number: prNumber,
});
return { return {
title: data.title, title: data.title,
body: data.body || '', body: data.body || "",
author: data.user?.login || '', author: data.user?.login || "",
labels: (data.labels || []).map((l) => (typeof l === 'string' ? l : l.name || '')), labels: (data.labels || []).map((l) =>
typeof l === "string" ? l : l.name || "",
),
branch: data.head.ref, branch: data.head.ref,
sha: data.head.sha, sha: data.head.sha,
}; };
@ -181,7 +216,7 @@ export async function fetchPR(
export async function fetchIssue( export async function fetchIssue(
owner: string, owner: string,
repo: string, repo: string,
issueNumber: number issueNumber: number,
): Promise<{ ): Promise<{
title: string; title: string;
body: string; body: string;
@ -191,19 +226,57 @@ export async function fetchIssue(
if (!octokit) return null; if (!octokit) return null;
try { try {
const { data } = await octokit.issues.get({ owner, repo, issue_number: issueNumber }); const { data } = await octokit.issues.get({
owner,
repo,
issue_number: issueNumber,
});
return { return {
title: data.title, title: data.title,
body: data.body || '', body: data.body || "",
author: data.user?.login || '', author: data.user?.login || "",
labels: (data.labels || []).map((l) => (typeof l === 'string' ? l : l.name || '')), labels: (data.labels || []).map((l) =>
typeof l === "string" ? l : l.name || "",
),
}; };
} catch (err) { } catch (err) {
getLogger().debug(`Failed to fetch issue ${owner}/${repo}#${issueNumber}`, err); getLogger().debug(
`Failed to fetch issue ${owner}/${repo}#${issueNumber}`,
err,
);
return null; return null;
} }
} }
export async function listAccessibleRepositories(): Promise<
Array<{ owner: string; repo: string }>
> {
if (!octokit) {
getLogger().debug("[dry-run] Cannot fetch repositories without a token");
return [];
}
const repos: Array<{ owner: string; repo: string }> = [];
for await (const response of octokit.paginate.iterator(
octokit.repos.listForAuthenticatedUser,
{
per_page: 100,
sort: "updated",
},
)) {
for (const repo of response.data) {
if (!repo.full_name || typeof repo.full_name !== "string") continue;
const parts = repo.full_name.split("/");
if (parts.length < 2 || !parts[0] || !parts[1]) continue;
const [owner, repoName] = parts;
repos.push({ owner, repo: repoName });
}
}
return repos;
}
export interface RecentComment { export interface RecentComment {
id: number; id: number;
body: string; body: string;
@ -216,10 +289,10 @@ export interface RecentComment {
export async function listRecentComments( export async function listRecentComments(
owner: string, owner: string,
repo: string, repo: string,
since: Date since: Date,
): Promise<RecentComment[]> { ): Promise<RecentComment[]> {
if (!octokit) { if (!octokit) {
getLogger().debug('[dry-run] Cannot fetch comments without a token'); getLogger().debug("[dry-run] Cannot fetch comments without a token");
return []; return [];
} }
@ -227,12 +300,15 @@ export async function listRecentComments(
const comments: RecentComment[] = []; const comments: RecentComment[] = [];
// Fetch recent issue comments // Fetch recent issue comments
const issueComments = await octokit.paginate(octokit.issues.listCommentsForRepo, { const issueComments = await octokit.paginate(
owner, octokit.issues.listCommentsForRepo,
repo, {
since: sinceIso, owner,
per_page: 100, repo,
}); since: sinceIso,
per_page: 100,
},
);
for (const comment of issueComments) { for (const comment of issueComments) {
if (!comment.body) continue; if (!comment.body) continue;
@ -240,9 +316,11 @@ export async function listRecentComments(
comments.push({ comments.push({
id: comment.id, id: comment.id,
body: comment.body, body: comment.body,
author: comment.user?.login || '', author: comment.user?.login || "",
createdAt: comment.created_at, createdAt: comment.created_at,
issueNumber: comment.issue_url ? parseInt(comment.issue_url.split('/').pop() || '0', 10) : 0, issueNumber: comment.issue_url
? parseInt(comment.issue_url.split("/").pop() || "0", 10)
: 0,
isPullRequest: false, // we'll determine this by fetching the issue isPullRequest: false, // we'll determine this by fetching the issue
}); });
} }
@ -256,19 +334,28 @@ function pickRandom(list: string[]): string {
} }
export function formatComment( export function formatComment(
responseConfig: ResponseConfig, responseConfig: {
type: 'issue' | 'pull_request', commentMarker: string;
includeConfidence: boolean;
includeReasoning: boolean;
messages: {
positive: string[];
negative: string[];
neutral: string[];
};
},
type: "issue" | "pull_request",
impact: string, impact: string,
confidence: number, confidence: number,
reasoning: string reasoning: string,
): string { ): string {
const typeLabel = type === 'pull_request' ? 'pull request' : 'issue'; const typeLabel = type === "pull_request" ? "pull request" : "issue";
const { messages } = responseConfig; const { messages } = responseConfig;
let messageList: string[]; let messageList: string[];
if (impact === 'positive') { if (impact === "positive") {
messageList = messages.positive; messageList = messages.positive;
} else if (impact === 'negative') { } else if (impact === "negative") {
messageList = messages.negative; messageList = messages.negative;
} else { } else {
messageList = messages.neutral; messageList = messages.neutral;
@ -276,14 +363,16 @@ export function formatComment(
const template = pickRandom(messageList); const template = pickRandom(messageList);
let body = responseConfig.commentMarker + '\n\n'; let body = responseConfig.commentMarker + "\n\n";
body += template.replace(/\{type\}/g, typeLabel).replace(/\{impact\}/g, impact); body += template
.replace(/\{type\}/g, typeLabel)
.replace(/\{impact\}/g, impact);
if (responseConfig.includeConfidence) { if (responseConfig.includeConfidence) {
body += `\n\n**Confidence:** ${(confidence * 100).toFixed(0)}%`; body += `\n\n**Confidence:** ${(confidence * 100).toFixed(0)}%`;
} }
if (responseConfig.includeReasoning) { if (responseConfig.includeReasoning) {
body += `\n\n**Analysis:** ${reasoning}`; body += `\n\n**Analysis:**\n\`\`\`\n${reasoning}\n\`\`\``;
} }
return body; return body;

View file

@ -16,7 +16,7 @@ export interface PollingConfig {
backfill?: boolean; backfill?: boolean;
stateFile?: string; stateFile?: string;
authorizedUsers?: string[]; authorizedUsers?: string[];
repositories?: RepoConfig[]; repositories?: RepoPattern[]; // Can include wildcards like [{ owner: 'NotAShelf', repo: '*' }]
} }
export interface ServerConfig { export interface ServerConfig {
@ -31,7 +31,7 @@ export interface DashboardConfig {
} }
export interface DashboardAuthConfig { export interface DashboardAuthConfig {
type: 'basic' | 'token'; type: "basic" | "token";
username?: string; username?: string;
password?: string; password?: string;
token?: string; token?: string;
@ -42,6 +42,11 @@ export interface RepoConfig {
repo: string; repo: string;
} }
export interface RepoPattern {
owner: string;
repo?: string; // undefined means all repos for that owner
}
export interface FiltersConfig { export interface FiltersConfig {
labels: { labels: {
include: string[]; include: string[];
@ -106,12 +111,30 @@ export interface LoggingConfig {
file: string; file: string;
} }
export type Impact = 'positive' | 'negative' | 'neutral'; export type Impact = "positive" | "negative" | "neutral";
export interface AnalysisResult { export interface AnalysisResult {
impact: Impact; impact: Impact;
confidence: number; confidence: number;
reasoning: string; reasoning: string;
// Multi-dimensional scoring for sophisticated analysis
dimensions?: {
correctness: number; // -1 to 1: will this work correctly?
risk: number; // -1 to 1: could this break things? (negative = risky)
maintainability: number; // -1 to 1: will this be maintainable?
alignment: number; // -1 to 1: does this fit the project?
};
// Correlation analysis
correlations?: {
suspiciousPatterns: string[];
reinforcingSignals: string[];
contradictions: string[];
};
// Uncertainty quantification
uncertainty?: {
confidenceInterval: [number, number]; // [lower, upper]
primaryUncertaintySource: string;
};
} }
export interface EngineBackend { export interface EngineBackend {
@ -121,7 +144,7 @@ export interface EngineBackend {
export interface WebhookEvent { export interface WebhookEvent {
action: string; action: string;
type: 'issue' | 'pull_request'; type: "issue" | "pull_request";
number: number; number: number;
title: string; title: string;
body: string; body: string;