PR Review

An intellectually honest review.

Made of building blocks you can understand, trust and build on.

Big diff, small change.

Abstraction decomposes a pull request into the changes that matter.

PR #4821 12 files +512 −131
billing/prorate.go
41 41 func ProrateSeats(sub Subscription, prev, next int) Charge {
42 - days := plan.CycleDays
42 + days := sub.CycleDays()
43 + delta := seatDelta(prev, next)
43 - if next < prev {
44 - return Charge{}
45 - }
44 + if delta == 0 {
45 + return Charge{}
46 + }
47 + remaining := sub.DaysLeft(now)
46 - amount := unit * (next - prev)
48 + amount := unit * delta * remaining / days
49 + seats := float64(delta*remaining) / float64(days)
47 - return Charge{Amount: amount, Seats: next - prev}
50 + return Charge{Amount: amount, Seats: seats}
48 51 }
52 +
53 + func seatDelta(prev, next int) int {
54 + d := next - prev
55 + if d < 0 {
56 + d = 0
57 + }
58 + return d
59 + }
billing/seats_test.go
12 12 func TestProrateSeats_MidCycle(t *testing.T) {
13 - sub := Subscription{Plan: monthly}
13 + sub := Subscription{Plan: monthly, StartedAt: now.AddDate(0, 0, -12)}
14 14 got := ProrateSeats(sub, 3, 5)
15 - want := unit * 2
15 + want := unit * 2 * 18 / 30
16 16 if got.Amount != want {
17 17 t.Fatalf("got %d, want %d", got.Amount, want)
18 18 }
19 19 }
20 +
21 + func TestSeatDelta_Downgrade(t *testing.T) {
22 + if d := seatDelta(5, 3); d != 0 {
23 + t.Fatalf("want 0, got %v", d)
24 + }
25 + }
billing/pb/seats.pb.go
1 1 // Code generated by protoc-gen-go. DO NOT EDIT.
2 - // protoc-gen-go v1.34.1
2 + // protoc-gen-go v1.36.2
3 3 type SeatChange struct {
4 - state protoimpl.MessageState
5 - sizeCache protoimpl.SizeCache
6 - unknownFields protoimpl.UnknownFields
7 -
8 - SeatCount int32 `protobuf:"varint,3,opt,name=seat_count,proto3"`
4 + state protoimpl.MessageState `protogen:"open.v1"`
5 + SeatCount int32 `protobuf:"varint,3,opt,name=seat_count,proto3"`
6 + unknownFields protoimpl.UnknownFields
7 + sizeCache protoimpl.SizeCache
9 8 }
10 9 func (x *SeatChange) GetSeatCount() int32 {
11 10 if x != nil {
12 11 return x.SeatCount
13 12 }
14 13 return 0
15 14 }
16 - var file_billing_seats_proto_rawDesc = []byte{
17 - 0x0a, 0x13, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x2f, 0x73, 0x65,
18 - 0x61, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x62,
15 + const file_billing_seats_proto_rawDesc = "" +
16 + "\n\x13billing/seats.proto\x12\abilling\"+\n" +
17 + "\nSeatChange\x12\x1d\n\nseat_count\x18\x03 \x01(\x05R\tseatCount"
593 more lines across 9 more files
Semantic diff 2 functions changed
ProrateSeats billing/prorate.go +9 −6

Prorates to the day using the subscription's cycle length, and now returns fractional seats.

41
func ProrateSeats(sub Subscription, prev, next int) Charge {
42
days := plan.CycleDayssub.CycleDays()
⋯ 5 lines
48
amount := unit * (next - prev)delta * remaining / days
49
seats := float64(delta*remaining) / float64(days)
seatDelta new billing/prorate.go +7

Returns how many seats were added. Downgrades clamp to zero.

10 files set aside: regenerated protobuf stubs and a rename. Tests are shown separately.

Small diff, big impact.

Abstraction follows a change into everything that calls it.

PR #4790 1 file +1 −1
accounts/subscription.ts
⋯ 40 unchanged lines
41 41 export function isActive(sub: Subscription): boolean {
42 - return sub.status === 'active';
42 + return sub.status === 'active' || sub.status === 'past_due';
43 43 }
⋯ 52 unchanged lines
That's the whole pull request.
Callers outside accounts 8 callers
  • lifecycle-emails sendPaymentReminders

    Payment reminders stop.

  • seats billSeatChanges

    Seats get billed to a declined card.

  • usage-export countPayingAccounts

    Past-due accounts count as paying.

  • entitlements canUsePremium

    Premium stays on after the card fails.

  • seats canAddSeat

    Past-due accounts can still add seats.

  • api-gateway rateLimitFor

    Unpaid accounts keep paid rate limits.

  • usage-export churnedAccounts

    Churn misses accounts that stopped paying.

  • lifecycle-emails sendWinBackOffer

    Win-back offers skip past-due accounts.

None of them appear in the diff.
How the review works

Facts you can verify.

Reasoning you can judge.

Agentic exploration rediscovers your codebase on every run, answering it differently. Abstraction compiles it once, keeps it current, and hands the facts to an LLM.

Computed facts

From the semantic model of your codebase. The same answer on every run.

  • Which functions a pull request changes, what was only renamed, and which code is generated.
  • Who calls each changed function, what it calls, and who else reads or writes the state it touches.
  • Your deterministic checks, on every statically traceable path, in milliseconds.

Reasoning

The LLM layer, based on the computed facts.

  • What each changed function now does, and whether that is a bug.
  • Whether a finding holds up. A separate step drops false positives before anything is posted.
  • The judgement calls no query can make, like your team’s conventions.
billing/prorate.go
48+ amount := unit * delta * remaining / days
49+ seats := float64(delta*remaining) / float64(days)
abstraction-com bot just now
High Exports under-bill mid-cycle upgrades

ExportCSV in invoices/export.go calls ProrateSeats and casts Seats to an int, so a 2.5-seat upgrade exports as 2 and the export stops matching the invoice.

Discuss ▶ Fix in…

The reach is computed. ExportCSV lives in another package, and no line of the diff touches it. The semantic model finds every caller of ProrateSeats and shows the reviewer how they use it.

The consequence is reasoned. Reading that call, the reviewer works out what a fractional seat count does to the export.

Tags

Name what matters.

Curate tags and roles for a shared vocabulary and fast lookups.

ExportCSV invoices/export.go
Tags
sensitivepiipublic Add tag

This function writes customer data to a bucket outside the system. Tag what the code touches, and add your own tags for what your team cares about.

Built in piisensitivesecurity Yours publichot pathbillingpayments

Once named, it can be found in one query and followed through every flow. The tag travels with the value until a sanitizer clears it, and that is what checks are written against.

phone Input · sensitive
hashPhone Sanitizer
customers.write Database writer
logger.info Logger
Sanitized before it is stored
Sensitive data reaches a log line
Checks

Make your intent a hard constraint.

Your rules, enforced on every pull request.

The rule

No PII in logs
let pii = variables().filterTag(Tag.Name, Tag.Email, Tag.Phone);
let logged = functions().filterRole(Role.Logger).params();
 
assertNoOverlap(pii.valueSinks(), logged);

Asked of our agent as "personal data never reaches a log line". It drafted the query from the codebase, and an admin approved it. Read aloud: the places personal data ends up share nothing with what loggers take in.

The counter-example

Abstraction · No PII in logs · Payments Failed

assertNoOverlap: 1 shared element

  1. api/invites.ts email Email
  2. → mail/queue.ts job.payload
  3. → log/logger.ts info(job) Logger

The invite handler's email flows into the queued job, and the queue hands every job to the logger. No line of the diff mentions a logger. The shared element is the value trail that breaks the rule.

A review comment that keeps coming back becomes a check. Nobody writes it twice.

  • 01 · Write

    Ask our agent in a sentence and the check runs on every repository in the workspace, or commit a check file to cover just that repository, like any CI check. Templates cover GDPR, entrypoints and more.

  • 02 · Scope

    Scope it to the paths, file types, tags or roles it cares about, so it runs only where they change.

  • 03 · Evaluate

    It queries the semantic model, not the text, so it catches what code reaches, not just what it mentions.

  • 04 · Gate

    Each check is its own GitHub check run. Set its severity to fail, and branch protection makes it a merge requirement.

CI proves the code builds. Checks hold it to your rules, whoever wrote it. Each check you add guards every pull request after it, so trust in the review compounds.
How checks work, in the docs
Verifiability

Trust the rules. Review the change.

Agents can fix what they can verify, so review can focus on the questions that need people.

Already automated
  • It compiles
  • Tests pass
  • Types check
  • Lint is clean
Before
Move the verifiability line
Checks all of them, on every pull request they apply to
No PII in logs AQL · Data flow
1 issue
Sensitive data is sanitized before storage AQL · Data flowAgent
Passed
Public endpoints are authenticated AQL · Reach
Passed
Prompts follow the house style Agent · on prompts
Passed
Annual plans never prorate AQL · Reach
New

AQL checks are deterministic queries over the semantic model. Agent checks take the rest.

With checks
Needs judgement
  • Does it do what it should?
  • Is this the right change?

Our agentic review helps. People decide.

A failed check is feedback an agent can act on before anyone reviews the pull request.

Review your next pull request with Abstraction.

We are onboarding teams in waves. Request access and we will reach out when your spot opens.

See pricing

ABSTRACTION

2026

Abstr AB · Org. no. 559512-2739