Skip to content

Feat/auto approve - #133

Merged
LinceMathew merged 17 commits into
masterfrom
feat/auto-approve
Oct 15, 2026
Merged

LinceMathew merged 17 commits into
masterfrom
feat/auto-approve

Conversation

@LinceMathew

@LinceMathew LinceMathew commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds risk-based auto-approval for pull requests, plus "Start a review" links in every new PR's description, across GitHub, GitLab, Gitea, Bitbucket and Azure DevOps.

PR review links

  • Every new PR/MR gets a LiveReview section in its description with Start a review and Review history links. It is updated in place, using hidden markdown markers.
  • The same section keeps an Auto-approval decisions table: the last 10 decisions, newest first, with the reason for each.
  • New webhook handling for PR open/update events on Gitea, Bitbucket and Azure DevOps, so links are added there too.

Auto-approve policy engine

  • A review started from the PR link runs a 5-stage check after it finishes: eligibility → static rules → risk score → LLM classification → final decision.
  • The result is posted back to the PR as a comment with the stage-by-stage breakdown, and as the provider's own verdict (approve / request changes, or vote on Azure).
  • Policies are set per org, with optional per-repo overrides (Settings → Auto Approve, and the repo's Auto Approve tab). The risk threshold has presets: Conservative (P20), Balanced (P50), Aggressive (P80), or a custom percentile.
  • Risk model: scores each review against the org's own recent reviews and learns from reported outcomes (revert / incident / confirmed safe). Includes an Auto Approve report (score distribution, decisions, calibration).
  • Force approve (with reason, audit-logged) for owners/admins or users at or above the policy's level.
  • The AI review prompt now also returns a diff classification (e.g. refactor-only, logic-change) with a confidence score, in the same call. This is used by the LLM stage.
  • Orgs without a policy are unaffected: the engine is off until a policy exists.

Review page

  • Auto Approve tab with the full stage-by-stage result.
  • Auto-approve stat (Approved / Not approved) next to Risk assessment.
  • When a review finishes while the page is open, it switches to Findings, and the Auto Approve tab shows a pulsing green/red dot until opened.

Other

  • Wording: "Not approved" instead of "Changes requested". Risk reasons show both the score and the percentile, e.g. "Risk score 0.97 (P70) is above the threshold of 0.74 (P50)".
  • Deleting a connector no longer fails on review commit history (review_commits.repository_id is now ON DELETE SET NULL).
  • Bitbucket token scope hints now list the write scopes needed to edit descriptions and post verdicts.
  • Org selector moved next to the logout icon; fixed a "No results" flash on the Review history link.

Database

Three additive migrations, applied automatically by dbmate up on start:

  • 20261012090000: pull_requests.links_comment_posted_at
  • 20261012100000: review_commits foreign key → ON DELETE SET NULL
  • 20261013090000: risk policy engine tables (auto_approve_policies, review_policy_log, review_policy_overrides, review_outcomes, risk_*, user_identities), users.review_level (default 1), pull_requests.author_email

No new environment variables, Docker dependencies or deploy-script changes.

Linked Issue

Validation

  • End-to-end on test PRs for GitHub, GitLab, Bitbucket and Azure DevOps: links added to the description, review started from the link, policy comment, verdict and decisions table posted.
  • npx tsc --noEmit passes.
  • make security-govulncheck: no vulnerabilities in called code.
  • make security-osv: no vulnerabilities (after the existing ignore list).
  • Gitea is not yet tested end to end.

Checklist

  • This PR fulfills an agreed issue.
  • I kept the change narrow and scoped.
  • I ran the most specific relevant validation and described it above.
  • If I changed behavior, I called that out clearly in this PR.
  • If I touched UI, I attached a GIF or video walkthrough. This is required.
  • If this change touches security, disclosure flow, credentials, storage, network behavior, or licensing/entitlement enforcement, I reviewed SECURITY.md and updated related documentation if needed.
  • I have read and accept the Contributor License Agreement.

Notes For Reviewers

  • Behaviour change for every connected repo: new PRs get a LiveReview section added to their description. It needs livereview_prod_url set; without it the section is skipped with a warning.
  • Auto-approval is opt-in per org; no verdicts are posted without a policy.
  • Bitbucket connectors need a token with write:pullrequest to edit descriptions and post verdicts. Without it, those calls are logged and skipped.
  • Design docs: docs/risk-policy-engine.md, docs/risk-policy-engine-six-pager.md, docs/pr-review-links-plan.md.

LinceMathew and others added 16 commits October 13, 2026 01:58
LiveReview Pre-Commit Check: ran (iter:1, coverage:0%)
Implements a layered funnel (eligibility -> static jq gates -> trained
risk score -> LLM classification -> decision) for the PR-link auto-approve
flow, replacing the previous "no critical findings" placeholder. Includes:

- Org/repo-scoped policy config (auto_approve_policies) with an
  online-learning risk model (internal/riskscore) anchored to a
  common-sense prior, category-level weight overrides, and daily +
  on-demand retraining.
- Static gate reuse via internal/canonicaldoc (shared with CI/CD rulesets).
- Eligibility by per-user review level, with identity mapping for PR
  authors and a whitelist bypass.
- Review detail "Auto Approve" tab, Settings policy editor (Risk Model /
  Static Rules / Eligibility / General / Production Feedback sub-tabs),
  and repository-level overrides reusing the same field components.
- Force Approve: an audited, level-gated override for a specific blocked
  review, and manual/webhook production-outcome reporting that feeds the
  model's online update.

Fixes found during review and testing: a malformed claim-UPDATE string
that broke Force Approve and landing-delay publish on every attempt, a
cold-start 500 on a never-scored org's risk-model endpoint, and a missing
org-ownership check on the manual outcome endpoint. Added regression tests
for all three plus policy resolution and canonical-doc scoping.

LiveReview Pre-Commit Check: skipped (iter:1, coverage:0%)
Risk-based auto-approve introduces a lot of unfamiliar settings (risk
threshold percentile, LLM confidence, blast radius weighting, landing
delay, force approve, etc.), so most users will not guess their meaning
from the label alone. Adds an "i" info icon with a hover tooltip next to
every control across the Settings policy editor, the repository override
section, and the review detail Auto Approve tab's funnel/force-approve/
outcome panels.

New shared pieces: InfoHint/FieldLabel (built on the existing Tooltip
primitive) and a single helpText.ts holding every hint's copy, written in
Simplified Technical English (short sentences, one idea each, concrete
examples) so it stays easy to review and keep consistent.

LiveReview Pre-Commit Check: skipped (iter:1, coverage:0%)
The funnel ran risk score before LLM classification and skipped the LLM
gate entirely ("skipped (earlier stage blocked)") whenever risk score
already failed. But the LLM gate makes no live call at that point — it's
a threshold check against the diffClassification the review-generation
prompt already wrote to metadata.review_result, available the whole time
the funnel runs. Evaluate risk and LLM independently so both verdicts
always show, and block on either; the final-decision reason now lists
every failing gate instead of only the first one found.

Also makes each policy funnel stage expandable in the Auto Approve tab,
showing the detail the backend already computed but didn't render: the
risk score's full feature-by-feature breakdown, the LLM gate's class and
confidence, static's matched ruleset, etc.

LiveReview Pre-Commit Check: skipped (iter:1, coverage:0%)
The risk model only let admins tune 4 generic signals (blast radius,
security findings, critical findings, diff size) even though every
finding is already classified into one of 10 categories (Security,
Reliability, Correctness, Performance, Scalability, Compliance &
Governance, Maintainability, Architecture, Developer Experience, Cost).
Add a feature + override slider for each of those categories, append-only
so old trained model versions keep scoring correctly. Security defaults
1.5x, Correctness/Reliability 1.25x, Performance 1.1x, everything else
1x, reflecting how most teams actually prioritize these.

Also:
- Debounce the category sliders (save 300ms after the user stops
  dragging, with instant local feedback) instead of firing a request and
  a full model refetch on every drag tick, which could visibly snap the
  thumb back mid-drag.
- Rename "Auto Approve" to "Auto Approve Policies" in the mega menu and
  Settings tab list so it's easier to find by search.
- Put the General sub-tab first, since it holds the settings people reach
  for most often.
- Format the Production Feedback webhook body as a real code block
  instead of a single dense inline string.
- Make every policy hover-hint readable: wider tooltip padding/line-height,
  explicit paragraph breaks instead of one run-on sentence, and each
  category's sub-issues rendered as a bulleted list instead of a long
  comma-separated clause.

LiveReview Pre-Commit Check: skipped (iter:1, coverage:0%)
LiveReview Pre-Commit Check: ran (iter:2, coverage:9%)
…r super admins

Every other owner-only link in megaMenuData.ts is gated by
`ctx.isSuperAdmin || ctx.orgRole === 'owner'`, but this one was missing the
isSuperAdmin fallback. A super admin could already open Settings -> Auto
Approve Policies directly (that tab's own gate, canManageCurrentOrg,
already accounts for isSuperAdmin), but the menu/search entry used a
stricter, inconsistent check and silently disappeared from both the
dropdown and Ctrl-K search, which share the same filtered list.

LiveReview Pre-Commit Check: skipped (iter:1, coverage:0%)
Two sliders (critical findings, diff size) were hidden behind a collapsed
"Other risk signals" details element, and blast radius sat at the very
bottom of that hidden group even though it's the strongest single risk
signal the model has - how far a change's effects reach, independent of
which finding category triggered it. Hiding any of these behind a fold
made them easy to miss and implied they mattered less than the finding
categories above them.

- Move blast radius to the top of the slider list and remove the
  collapsed section entirely, so every weight dial is visible by default.
- Give blast radius the highest default multiplier (2.0x), ahead of
  security's 1.5x, matching how strongly it should influence scoring.

LiveReview Pre-Commit Check: skipped (iter:1, coverage:0%)
…table report

The risk-based auto-approve policy engine (internal/policyengine,
internal/riskscore) was already built and wired up, but there was no way
to verify it behaved sensibly on common-sense inputs, no proof the ML
feedback loop actually learns anything, and no explanation anyone outside
the implementation could follow.

Adds:
- A 46-case scenario corpus (internal/policyengine/testdata/scenarios.json)
  run against the real engine.RunWithPolicy dry-run path, covering every
  funnel stage, all 10 finding-taxonomy categories, mode shortcuts,
  threshold boundaries, and daily caps/delays.
- Formal-ish property/unit tests on the model's math (category-override
  precedence, prior monotonicity, anchor decay) and DB-backed tests on the
  learning paths (batch refit's promotion gate, the real-time online nudge,
  the /simulate handler's no-side-effects guarantee).
- A threshold experiment (internal/riskscore/threshold_experiment_test.go)
  computing real approval rates at P0/P10/P20/P50/P80/P100 against a
  shared, reproducible reference distribution
  (internal/riskscore/referencemix), proving the percentile setting and
  the cutoff it resolves to are two different things.
- A learning-cycle test showing a review's score move 0.668 -> 0.975 ->
  0.999 as synthetic production outcomes are reported against it, with the
  full before/after weight vector logged, not just the score.
- docs/risk-policy-explainability.md and
  docs/risk-policy-verification-report.html (an interactive report with a
  live score calculator, sortable results/threshold tables, and a full
  signal reference) explaining all of this at CTO/manager/engineer depth.
- scripts/regen_risk_report.py (make regen-risk-report): regenerates every
  number in the HTML report directly from the three tests' structured
  log output, so the report can't silently drift from the model it
  describes. Refuses to run if any test fails.

Every test cleans up everything it writes (verified by running the full
suite twice and checking row counts return to zero), so it's safe to run
repeatedly against any database, including a live instance.

LiveReview Pre-Commit Check: skipped (iter:1, coverage:0%)
…ration

The auto-approve policy's only production visibility was one-review-at-a-
time (the explain panel) and one-shot historical replay (Simulate). There
was no way to see how the policy is actually behaving on an org's real,
live traffic: where scores land relative to the threshold, how many PRs
are being approved/blocked/overridden, or how often a reported outcome
disagreed with the decision.

Adds:
- GET /api/v1/auto-approve-insights (internal/api/auto_approve_insights_handlers.go):
  aggregates review_policy_log, risk_feature_snapshots, review_policy_overrides,
  and review_outcomes (excluding source='test', same exclusion the training
  path uses) over a selectable 7/30/90-day window into a score histogram
  with the resolved cutoff, decision counts/trend/override rate, and an
  outcome-calibration cross-tab with a false-approve rate.
- A Reports -> Auto Approve tab (ui/src/pages/Reports/AutoApproveReport.tsx)
  rendering all three, plus a mega-menu entry and a link from Settings'
  existing Production Feedback tab.
- A handler test seeding two orgs and asserting both the aggregation math
  and tenant isolation (no org can see another org's counts/snapshots/
  outcomes/overrides).

LiveReview Pre-Commit Check: skipped (iter:1, coverage:0%)
LiveReview Pre-Commit Check: ran (iter:1, coverage:0%)
LiveReview Pre-Commit Check: skipped (iter:1, coverage:0%)
Adds the Auto-Approve pitch (Meta RADAR-inspired risk-based auto-approval,
three risk signals, the Track/Learn/Automate/Improve playbook) ahead of the
existing blast-radius explanation, mirroring the site's new hero ordering.
Adds an Auto-Approve anchor and wires it into the pillar table and the
Get Started nav table.

LiveReview Pre-Commit Check: skipped (iter:1, coverage:0%)
Auto-Approve now comes first as its own section (matches the site's new
hero ordering) with a clickable demo video; the existing blast-radius
content becomes its own "Within-Diff Prioritization" section below it,
since the two are different capabilities. Updates the nav table's link
to the relocated blast-radius content accordingly.

Note: the Auto-Approve demo is committed to the repo and linked as a
clickable thumbnail rather than an inline player, since GitHub only
auto-embeds videos uploaded through its web drag-and-drop flow, not
ones committed directly to a repo.

LiveReview Pre-Commit Check: skipped (iter:2, coverage:0%)
- "Not approved" instead of "Changes requested" in PR comments and the decisions table
- Risk threshold presets (Conservative/Balanced/Aggressive) with a P-marked custom slider
- Risk reason shows score and threshold with their percentiles
- Auto-approve stat next to Risk assessment, pulsing dot on the Auto Approve tab

LiveReview Pre-Commit Check: vouched (iter:3, coverage:0%)
Drops pg_dump version, River job table/index and review_events sequence drift; keeps this branch's migration changes.

LiveReview Pre-Commit Check: vouched (iter:1, coverage:0%)
@LiveReview-Bot

Copy link
Copy Markdown

Implement Risk-Based Auto-Approval Policy Engine

Overview

This change introduces a new risk-based auto-approval policy engine. It evaluates pull requests using a multi-layered architecture. This system aims to reduce reviewer fatigue and focus human attention on high-risk changes.

Technical Highlights

  • db/schema.sql: Adds new tables for review events, policy logs, risk factors, and user identities.
  • docs/risk-policy-engine-six-pager.md, docs/risk-policy-engine.md: Document the multi-layered policy evaluation funnel and continuous learning loop.
  • internal/ai/gemini/batch.go, internal/ai/gemini/gemini.go, internal/ai/langchain/provider.go: Extend AI providers to include DiffClassification for semantic insights.
  • internal/api/auto_approve_insights_handlers.go: Implements API endpoints for monitoring auto-approval policy performance and calibration.
  • internal/api/auto_approve_policy_handlers.go: Adds CRUD and simulation API endpoints for managing auto-approve policies.
  • internal/api/review_policy_actions.go: Introduces API handlers for viewing policy logs, force-approving, and marking review outcomes.
  • go.mod: Adds github.com/cdipaolo/goml as a new machine learning dependency.
  • internal/api/ci_rulesets_handler.go: Refactors canonical document generation and JQ evaluation into a shared package.
  • internal/api/connectors.go: Defers webhook removal job queuing until after successful database deletion.
  • internal/api/pull_request_review_trigger.go: Adds an endpoint to resolve organization ID from a pull request ID.

Impact

  • Functionality: Users can configure and monitor automated pull request approval based on risk scores.
  • Risk: Extensive database schema changes and new dependencies require careful migration and performance monitoring.

Comment thread db/schema.sql
review_id bigint NOT NULL,
org_id bigint NOT NULL,
ts timestamp with time zone DEFAULT now() NOT NULL,
event_type text NOT NULL,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The event_type field is currently a text type. Consider using an enum for stricter data validation and improved query performance.

Suggestions:

  1. Use a CHECK constraint or PostgreSQL ENUM type for event_type to restrict values and improve data integrity.
  2. Document expected event_type values.

Comment thread db/schema.sql
org_id bigint NOT NULL,
ts timestamp with time zone DEFAULT now() NOT NULL,
event_type text NOT NULL,
level text,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The level field is currently a text type. Consider using an enum for stricter data validation and improved query performance.

Suggestions:

  1. Use a CHECK constraint or PostgreSQL ENUM type for level to restrict values and improve data integrity.
  2. Document expected level values.

Comment thread db/schema.sql
ts timestamp with time zone DEFAULT now() NOT NULL,
event_type text NOT NULL,
level text,
batch_id text,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

The batch_id field is currently a text type. If it represents a UUID, consider using the UUID data type.

Suggestions:

  1. If batch_id is always a UUID, use uuid type for better type safety and storage efficiency.

Comment thread db/schema.sql
review_id bigint NOT NULL,
org_id bigint NOT NULL,
reported_by_user_id bigint,
outcome_type text NOT NULL,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

The outcome_type field is currently a text type. While it already has a CHECK value, using an enum would make the possible values more explicit.

Suggestions:

  1. Consider using a PostgreSQL ENUM type for outcome_type for stronger type enforcement and clarity, instead of a CHECK constraint.

Comment thread db/schema.sql
org_id bigint NOT NULL,
reported_by_user_id bigint,
outcome_type text NOT NULL,
source text NOT NULL,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

The source field is currently a text type. While it already has a CHECK value, using an enum would make the possible values more explicit.

Suggestions:

  1. Consider using a PostgreSQL ENUM type for source for stronger type enforcement and clarity, instead of a CHECK constraint.

Comment thread db/schema.sql
id bigint NOT NULL,
review_id bigint NOT NULL,
org_id bigint NOT NULL,
stage text NOT NULL,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The stage field is currently a text type. Consider using an enum for stricter data validation and improved query performance.

Suggestions:

  1. Use a CHECK constraint or PostgreSQL ENUM type for stage to restrict values and improve data integrity.
  2. Document expected stage values.

Comment thread db/schema.sql
review_id bigint NOT NULL,
org_id bigint NOT NULL,
stage text NOT NULL,
verdict text NOT NULL,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The verdict field is currently a text type. Consider using an enum for stricter data validation and improved query performance.

Suggestions:

  1. Use a CHECK constraint or PostgreSQL ENUM type for verdict to restrict values and improve data integrity.
  2. Document expected verdict values.

Comment thread db/schema.sql
CREATE TABLE public.risk_factor_overrides (
id bigint NOT NULL,
org_id bigint NOT NULL,
feature_category text NOT NULL,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The feature_category field is currently a text type. Consider using an enum for stricter data validation and improved query performance.

Suggestions:

  1. Use a CHECK constraint or PostgreSQL ENUM type for feature_category to restrict values and improve data integrity.
  2. Document expected feature_category values.

Comment thread db/schema.sql

CREATE TABLE public.risk_model_versions (
id bigint NOT NULL,
org_id bigint,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

The org_id field is nullable. This implies global models. Please clarify the intended behavior and scope.

Suggestions:

  1. Add a comment explaining when org_id is NULL (e.g., for global default models).
  2. Ensure application logic correctly handles NULL org_id for model selection.

Comment thread db/schema.sql
trained_at timestamp with time zone DEFAULT now() NOT NULL,
training_sample_count integer DEFAULT 0 NOT NULL,
holdout_auc double precision,
update_kind text NOT NULL,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

The update_kind field is currently a text type. While it already has a CHECK value, using an enum would make the possible values more explicit.

Suggestions:

  1. Consider using a PostgreSQL ENUM type for update_kind for stronger type enforcement and clarity, instead of a CHECK constraint.

Comment thread db/schema.sql

CREATE TABLE public.upgrade_request_events (
id bigint DEFAULT nextval('public.upgrade_request_events_id_seq'::regclass) NOT NULL,
upgrade_request_id character varying(36) NOT NULL,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The upgrade_request_id field is of type character varying(36). If it represents a UUID, please use the uuid data type.

Suggestions:

  1. Change upgrade_request_id to uuid type for better type safety, validation, and storage efficiency.

Comment thread db/schema.sql
id bigint DEFAULT nextval('public.upgrade_request_events_id_seq'::regclass) NOT NULL,
upgrade_request_id character varying(36) NOT NULL,
org_id bigint NOT NULL,
event_source character varying(64) NOT NULL,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The event_source field is of type character varying(64). Consider using an enum for stricter data validation.

Suggestions:

  1. Use a CHECK constraint or PostgreSQL ENUM type for event_source to restrict values and improve data integrity.
  2. Document expected event_source values.

Comment thread db/schema.sql
upgrade_request_id character varying(36) NOT NULL,
org_id bigint NOT NULL,
event_source character varying(64) NOT NULL,
event_type character varying(64) NOT NULL,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The event_type field is of type character varying(64). Consider using an enum for stricter data validation.

Suggestions:

  1. Use a CHECK constraint or PostgreSQL ENUM type for event_type to restrict values and improve data integrity.
  2. Document expected event_type values.

is_active BOOLEAN NOT NULL DEFAULT false
);

CREATE INDEX idx_risk_model_versions_org_active ON risk_model_versions (org_id) WHERE is_active;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

The index on org_id and is_active for risk_model_versions is critical for the performance of active model lookups.

feature_category TEXT NOT NULL,
multiplier DOUBLE PRECISION NOT NULL DEFAULT 1.0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (org_id, feature_category)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

The UNIQUE (org_id, feature_category) constraint on risk_factor_overrides ensures that there is only one override per category per organization.

Comment thread db/schema.sql
raw_llm_output text NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
context text[],
context jsonb,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The type change for chat_charts.context from text[] to jsonb requires ensuring that all code paths correctly handle jsonb.

Suggestions:

  1. Verify all application code interacting with chat_charts.context is updated for jsonb type.

Comment thread db/schema.sql
data bytea NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
context text[]
context jsonb

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The type change for chat_files.context from text[] to jsonb requires ensuring that all code paths correctly handle jsonb.

Suggestions:

  1. Verify all application code interacting with chat_files.context is updated for jsonb type.

Comment thread db/schema.sql
ALTER SEQUENCE public.review_commits_id_seq OWNED BY public.review_commits.id;


--

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The review_events table has been removed. Please confirm that this deletion is intentional and that no data loss has occurred.

Suggestions:

  1. Confirm review_events table removal was intentional and data migration/archiving was handled if necessary.

Comment thread db/schema.sql
event_type text NOT NULL,
level text,
batch_id text,
data jsonb NOT NULL

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The jsonb column data is marked as NOT NULL but lacks a default empty object. If empty data is considered valid, please add DEFAULT '{}'::jsonb.

Suggestions:

  1. Change data jsonb NOT NULL to data jsonb DEFAULT '{}'::jsonb NOT NULL if empty JSON is a valid state.

Comment thread db/schema.sql
id bigint NOT NULL,
review_id bigint NOT NULL,
org_id bigint NOT NULL,
reported_by_user_id bigint,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

The reported_by_user_id column is nullable. Please clarify whether system-generated outcomes should link to a specific system user ID or if NULL is acceptable.

Suggestions:

  1. Add a comment explaining when reported_by_user_id can be NULL.
  2. Consider a default system user ID if all outcomes must be attributable.

Comment thread db/schema.sql
org_id bigint NOT NULL,
feature_category text NOT NULL,
multiplier double precision DEFAULT 1.0 NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The updated_at column has DEFAULT now() but no ON UPDATE trigger. The application must manually update this field.

Suggestions:

  1. Implement an ON UPDATE trigger for updated_at to automatically update on row modification.
  2. Ensure all application updates to risk_factor_overrides explicitly set updated_at.

Comment thread db/schema.sql
review_id bigint NOT NULL,
org_id bigint NOT NULL,
model_version integer NOT NULL,
features jsonb NOT NULL,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The jsonb column features is marked as NOT NULL but lacks a default empty object. If empty features are considered valid, please add DEFAULT '{}'::jsonb.

Suggestions:

  1. Change features jsonb NOT NULL to features jsonb DEFAULT '{}'::jsonb NOT NULL if empty JSON is a valid state.

Comment thread db/schema.sql

CREATE TABLE public.risk_model_versions (
id bigint NOT NULL,
org_id bigint,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

Consider adding a unique constraint on (org_id, version) for risk_model_versions to prevent duplicate versions within an organization.

Suggestions:

  1. Add ALTER TABLE public.risk_model_versions ADD CONSTRAINT risk_model_versions_org_id_version_key UNIQUE (org_id, version);

Comment thread db/schema.sql
training_sample_count integer DEFAULT 0 NOT NULL,
holdout_auc double precision,
update_kind text NOT NULL,
is_active boolean DEFAULT false NOT NULL,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

An is_active flag exists. If only one model can be active per org_id (or globally), please add a partial unique index.

Suggestions:

  1. Add CREATE UNIQUE INDEX risk_model_versions_active_idx ON public.risk_model_versions (org_id) WHERE is_active = true; to enforce single active model per org.

Comment thread db/schema.sql

CREATE TABLE public.upgrade_request_events (
id bigint DEFAULT nextval('public.upgrade_request_events_id_seq'::regclass) NOT NULL,
upgrade_request_id character varying(36) NOT NULL,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The upgrade_request_id is currently character varying(36). If this field represents a UUID, using the uuid type would be more efficient for storage and validation.

Suggestions:

  1. Change upgrade_request_id character varying(36) to upgrade_request_id uuid.

Comment thread db/schema.sql
value text NOT NULL,
created_by bigint,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The updated_at column has DEFAULT now() but no ON UPDATE trigger. The application must manually update this field.

Suggestions:

  1. Implement an ON UPDATE trigger for updated_at to automatically update on row modification.
  2. Ensure all application updates to user_identities explicitly set updated_at.

Comment thread db/schema.sql
created_by bigint,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT user_identities_kind_check CHECK ((kind = ANY (ARRAY['email'::text, 'username'::text])))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

Add a unique constraint on (org_id, kind, value) to prevent duplicate identities for the same user and organization.

Suggestions:

  1. Add ALTER TABLE public.user_identities ADD CONSTRAINT user_identities_org_id_kind_value_key UNIQUE (org_id, kind, value);

Comment thread db/schema.sql

ALTER TABLE ONLY public.review_commits
ADD CONSTRAINT review_commits_repository_id_fkey FOREIGN KEY (repository_id) REFERENCES public.repositories(id);
ADD CONSTRAINT review_commits_repository_id_fkey FOREIGN KEY (repository_id) REFERENCES public.repositories(id) ON DELETE SET NULL;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The foreign key ON DELETE SET NULL changes the behavior; therefore, repository_id must be nullable.

Suggestions:

  1. Confirm review_commits.repository_id column is nullable to avoid constraint errors on repository deletion.

Comment thread db/schema.sql
--

ALTER TABLE ONLY public.upgrade_request_events
ADD CONSTRAINT upgrade_request_events_org_id_fkey FOREIGN KEY (org_id) REFERENCES public.orgs(id) ON DELETE CASCADE;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The foreign key ON DELETE CASCADE implies that the history in upgrade_request_events is not needed after an organization is deleted.

Suggestions:

  1. Verify ON DELETE CASCADE is the intended behavior; consider SET NULL or NO ACTION if event history must persist for auditing or debugging.

Comment thread db/schema.sql
--

ALTER TABLE ONLY public.upgrade_request_events
ADD CONSTRAINT upgrade_request_events_upgrade_request_id_fkey FOREIGN KEY (upgrade_request_id) REFERENCES public.upgrade_requests(upgrade_request_id) ON DELETE CASCADE;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The foreign key ON DELETE CASCADE implies that the history in upgrade_request_events is not needed after a request is deleted.

Suggestions:

  1. Verify ON DELETE CASCADE is the intended behavior; consider SET NULL or NO ACTION if event history must persist for auditing or debugging.

| Auth | Both links require a LiveReview login. Logged-out users log in and are sent back to the link (existing `redirectAfterLogin` flow). |
| Org | The user is switched to the PR's org automatically. Non-members get "not found". |
| Trigger behavior | Opening the trigger link starts the review immediately (no confirm page, for now) |
| Repeat clicks | Each click starts a new review, **unless** a review for the PR is `created`/`in_progress` and started less than 10 minutes ago. Then it redirects to that review. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

A 10-minute reuse window for repeat clicks is a good mitigation for accidental review starts from link previewers.


1. Skip unless `event.Metadata["action"]` is `opened` (GitHub) or `open` (GitLab).
2. Claim the post atomically:
`UPDATE pull_requests SET links_comment_posted_at = now() WHERE id = $1 AND links_comment_posted_at IS NULL`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

The atomic update for links_comment_posted_at prevents race conditions that could occur from concurrent webhook deliveries.

3. Load the repo token via the existing `repositoryConnectorInfo` lookup.
4. Read the public URL via the webhook store's `GetWebhookPublicEndpoint` (inject the store
into the worker in `jobqueue.go`).
5. Read the description, append the section if the marker is missing, and save it. On failure, reset `links_comment_posted_at` to `NULL` and return the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

Resetting links_comment_posted_at on failure ensures that River retries and prevents a stale state.

middleware chain can't be used here (approved exception).
- Handler looks up `pull_requests.org_id` and verifies the user is an active member of that
org. Returns `{ "org_id": ... }`, or **404** if the PR doesn't exist or the user isn't a
member (no existence leak).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

Returning a 404 for a non-existent PR or a non-member is a good security practice as it prevents existence leaks.

filter is an option on the existing Reviews page.

## Known limitations

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

The risk of link previewers initiating reviews has been acknowledged, and it's good to document this and plan for a future confirmation page.

In plain terms, the model works like this:

```
z = bias + sum over every signal of (its value x its weight x its category importance)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The formula variables 'value', 'weight', and 'category importance' lack clear definitions. Please clarify how each of these is determined (e.g., raw, normalized, learned, or configured).

Suggestions:

  1. Add a brief explanation for each term in the formula (e.g., 'its value' is the normalized signal, 'its weight' is learned, 'its category importance' is configured).

"safe enough" means something different in those two situations.

Finally, the model watches its own track record. Every outcome reported
from production — a revert, an incident, a confirmed-safe merge — nudges

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The phrase 'small, careful amount' for model nudges is vague. Please specify how this amount is determined (e.g., fixed learning rate, adaptive, or bounded).

Suggestions:

  1. Elaborate on the mechanism for determining the 'small, careful amount' for real-time nudges, including any safeguards against over-correction.


## Risks and What We're Watching

A brand-new customer org has no score history to calibrate a percentile

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The 'conservative fixed cutoff' for new organizations lacks specifics. Please define its value and whether it is configurable or dynamically set.

Suggestions:

  1. Specify the default value of the 'conservative fixed cutoff' and clarify if it's a global constant, configurable, or derived from a baseline.

specifically so that no single signal is ever the only thing standing
between a risky change and production.

One capability was scoped and deliberately set aside for a later phase: a

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

The observe-only mode is critical for building trust and adoption. Consider prioritizing its implementation earlier for new customers.

Suggestions:

  1. Re-evaluate the priority of the 'observe-only mode' feature, as it can significantly reduce friction and build confidence for new customer onboarding.

require_llm_confidence INTEGER NOT NULL DEFAULT 8, -- out of 10, mirrors RADAR's ACR gate
blocking_ruleset_ids BIGINT[] NOT NULL DEFAULT '{}', -- reuses ci_rulesets.id
landing_delay_seconds INTEGER NOT NULL DEFAULT 0, -- post-approve human-rejection window
daily_approve_cap INTEGER, -- NULL = unlimited

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

For daily_approve_cap, the scope (organization/repository) and time window (rolling 24 hours/calendar day) are missing.

Suggestions:

  1. Specify if the cap is per-org or per-repo.
  2. Clarify the time window for the daily cap (e.g., rolling 24 hours, UTC calendar day).

they're part of that stage's design, not generic schema.)

**`review_outcomes`** (new table) for the feedback loop: `review_id, org_id,
reported_by_user_id, outcome_type ('revert'|'incident'|'confirmed_safe'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

For review_outcomes.source set to 'ci', please clarify the security and authentication mechanisms for automated CI reporting.

Suggestions:

  1. Detail how CI systems will authenticate and authorize to report outcomes, especially if restrict_outcome_reporting_to_admins is true.

@LinceMathew
LinceMathew merged commit dd28a76 into master Oct 15, 2026
16 checks passed
@LinceMathew
LinceMathew deleted the feat/auto-approve branch October 17, 2026 14:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants