test(explore): #1500 regression fixtures for budget allocation (CG-6)

Two permanent fixtures pinning the failure mode from issue #1500 — explore
spending its byte envelope on files that merely name-collide with the query.
BOTH FAIL TODAY, by design: they document the bug and become the pass gate
for CG-10 (scoring) + CG-12 (proportional allocation).

__tests__/fixtures/payroll-go/ — a synthetic Go service mirroring the
reporter's shape: generated FKIT CRUD beside a hand-written payroll use-case,
entered from an HTTP route. Half the generated tree carries ORDINARY names
detectable only by their `// Code generated ... DO NOT EDIT.` header (the
#1500 case, and end-to-end cover for CG-5); `payrollpb/*.pb.go` covers the
path-detectable channel. BuildPayslip, Upsert and Store each exist twice,
generated and hand-written. cycle.go sits above the whole-file window so it
clips; the generated files sit below it so they ship whole.

Asking "how does payroll cycle create and calculate payslips?" — naming none
of the answering symbols — the generated CRUD delivers 57.4% of the envelope
against the hand-written layer's 25.6%, all of the latter domain types.
cycle.go is allocated the single largest slice (30.6%) and delivers ZERO: the
hard ceiling drops its whole section. runPayrollCycleAll, the hand-written
BuildPayslip and the real Upsert never reach the agent.

The second fixture is this repo, "how does explore allocate its output budget
across files", where scripts/agent-eval/*.mjs take 71.8% against tools.ts's
18.5% despite scoring 4.6x lower. It reads the live index, so its assertions
are relative rather than fixed percentages.

- scripts/agent-eval/probe-allocation.mjs — per-file budget-share probe,
  driving the CG-4 diagnostic through a JSONL sidecar so it measures the
  shipping allocator. Fixture entries are hermetic (copy + re-index per run,
  verified byte-identical across runs); exits 1 while any assertion fails.
- scripts/agent-eval/allocation-fixtures.json — both fixtures declared, with
  the 2026-08-03 baselines.
- __tests__/explore-allocation-1500.test.ts — fixture-shape assertions green
  today; the allocation assertions held as `it.fails` so the suite stays green
  while the bug is open and goes RED the moment it is fixed.

Also documented and deliberately left unfixed: runPayrollCycleAll's
`s.store.Upsert` edge resolves to the GENERATED Store.Upsert, not the
hand-written one — same-name method resolution across two packages picks the
wrong receiver. It is upstream of the allocation bug, so it belongs with
CG-10's scoring work.

Refs #1500

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Colby McHenry
2026-08-03 23:30:17 -05:00
co-authored by Claude Opus 5
parent 16e17495f4
commit bd86ad2061
25 changed files with 2687 additions and 0 deletions
@@ -0,0 +1,227 @@
package payroll
import (
"context"
"errors"
"fmt"
"sort"
"time"
"github.com/example/payroll-svc/internal/domain/payroll"
"github.com/example/payroll-svc/internal/platform/clock"
"github.com/example/payroll-svc/internal/store/payslipstore"
)
// ErrCycleClosed is returned when a cycle has already been finalized.
var ErrCycleClosed = errors.New("payroll cycle is closed")
// ErrNoEmployees is returned when a cycle resolves to an empty roster.
var ErrNoEmployees = errors.New("payroll cycle has no active employees")
// RunOptions tunes a single run of a payroll cycle.
type RunOptions struct {
// DryRun computes every payslip but persists nothing.
DryRun bool
// Reason is recorded on the audit trail for re-runs.
Reason string
// Only, when non-empty, restricts the run to these employee ids.
Only []string
}
// RunResult is the outcome of one payroll cycle run.
type RunResult struct {
CycleID string
Payslips []payroll.Payslip
TotalGrossCents int64
TotalNetCents int64
Skipped []string
FinishedAt time.Time
}
// Service is the hand-written payroll use-case layer. It owns the order of
// operations for a cycle: resolve the roster, build a payslip per employee,
// then persist. The generated CRUD layer under internal/gen has no opinion
// about any of that — it can only read and write single rows.
type Service struct {
store *payslipstore.Store
clock clock.Clock
}
func NewService(store *payslipstore.Store, c clock.Clock) *Service {
return &Service{store: store, clock: c}
}
// RunCycle is the public entry point used by the HTTP handler. It loads the
// cycle, guards its state, and delegates the actual work to runPayrollCycleAll.
func (s *Service) RunCycle(ctx context.Context, cycleID string, opts RunOptions) (RunResult, error) {
cycle, err := s.loadCycle(ctx, cycleID)
if err != nil {
return RunResult{}, err
}
if cycle.Status == payroll.CycleClosed {
return RunResult{}, ErrCycleClosed
}
roster, err := s.rosterFor(ctx, cycle, opts)
if err != nil {
return RunResult{}, err
}
if len(roster) == 0 {
return RunResult{}, ErrNoEmployees
}
return s.runPayrollCycleAll(ctx, cycle, roster, opts)
}
// runPayrollCycleAll is the heart of the cycle: for every employee on the
// roster it builds a payslip from that employee's contract and timesheet,
// then upserts the result. Ordering matters — a payslip is only persisted
// after every earning, deduction and tax line has been resolved, so a
// partially-computed slip can never reach the store.
func (s *Service) runPayrollCycleAll(
ctx context.Context,
cycle payroll.Cycle,
roster []payroll.Employee,
opts RunOptions,
) (RunResult, error) {
result := RunResult{CycleID: cycle.ID}
now := s.clock.Now()
for _, employee := range roster {
if err := ctx.Err(); err != nil {
return result, err
}
timesheet, err := s.timesheetFor(ctx, cycle, employee)
if err != nil {
result.Skipped = append(result.Skipped, employee.ID)
continue
}
slip, err := s.BuildPayslip(ctx, cycle, employee, timesheet)
if err != nil {
return result, fmt.Errorf("build payslip for %s: %w", employee.ID, err)
}
slip.RunAt = now
slip.RunReason = opts.Reason
if !opts.DryRun {
if err := s.store.Upsert(ctx, slip); err != nil {
return result, fmt.Errorf("persist payslip for %s: %w", employee.ID, err)
}
}
result.Payslips = append(result.Payslips, slip)
result.TotalGrossCents += slip.GrossCents
result.TotalNetCents += slip.NetCents
}
if !opts.DryRun {
if err := s.closeCycle(ctx, cycle, now); err != nil {
return result, err
}
}
sort.Slice(result.Payslips, func(i, j int) bool {
return result.Payslips[i].EmployeeID < result.Payslips[j].EmployeeID
})
result.FinishedAt = now
return result, nil
}
// rosterFor resolves which employees this cycle pays. An employee joins the
// roster when their contract overlaps the cycle window and they are not on
// unpaid leave for the whole period.
func (s *Service) rosterFor(ctx context.Context, cycle payroll.Cycle, opts RunOptions) ([]payroll.Employee, error) {
all, err := s.store.EmployeesForCycle(ctx, cycle.ID)
if err != nil {
return nil, err
}
only := map[string]bool{}
for _, id := range opts.Only {
only[id] = true
}
roster := make([]payroll.Employee, 0, len(all))
for _, e := range all {
if len(only) > 0 && !only[e.ID] {
continue
}
if !e.Contract.OverlapsWindow(cycle.Start, cycle.End) {
continue
}
if e.UnpaidLeaveCoversWindow(cycle.Start, cycle.End) {
continue
}
roster = append(roster, e)
}
sort.Slice(roster, func(i, j int) bool { return roster[i].ID < roster[j].ID })
return roster, nil
}
func (s *Service) timesheetFor(ctx context.Context, cycle payroll.Cycle, e payroll.Employee) (payroll.Timesheet, error) {
ts, err := s.store.Timesheet(ctx, cycle.ID, e.ID)
if err != nil {
return payroll.Timesheet{}, err
}
if ts.Approved {
return ts, nil
}
if e.Contract.Kind == payroll.ContractSalaried {
// Salaried staff are paid the contractual period regardless of an
// unapproved timesheet; hourly staff are skipped until approval.
return payroll.Timesheet{
CycleID: cycle.ID,
EmployeeID: e.ID,
Approved: true,
Units: e.Contract.PeriodUnits(cycle.Start, cycle.End),
}, nil
}
return payroll.Timesheet{}, fmt.Errorf("timesheet for %s not approved", e.ID)
}
func (s *Service) loadCycle(ctx context.Context, cycleID string) (payroll.Cycle, error) {
if cycleID == "" {
return payroll.Cycle{}, errors.New("empty cycle id")
}
return s.store.Cycle(ctx, cycleID)
}
func (s *Service) closeCycle(ctx context.Context, cycle payroll.Cycle, at time.Time) error {
cycle.Status = payroll.CycleClosed
cycle.ClosedAt = at
return s.store.SaveCycle(ctx, cycle)
}
// Cycle exposes a cycle for the read endpoints.
func (s *Service) Cycle(ctx context.Context, cycleID string) (payroll.Cycle, error) {
return s.loadCycle(ctx, cycleID)
}
// PayslipsForCycle lists the payslips a completed cycle produced.
func (s *Service) PayslipsForCycle(ctx context.Context, cycleID string) ([]payroll.Payslip, error) {
slips, err := s.store.ListByCycle(ctx, cycleID)
if err != nil {
return nil, err
}
sort.Slice(slips, func(i, j int) bool { return slips[i].EmployeeID < slips[j].EmployeeID })
return slips, nil
}
// Reopen unwinds a closed cycle so it can be re-run after a correction.
func (s *Service) Reopen(ctx context.Context, cycleID string, reason string) error {
cycle, err := s.loadCycle(ctx, cycleID)
if err != nil {
return err
}
if cycle.Status != payroll.CycleClosed {
return nil
}
cycle.Status = payroll.CycleOpen
cycle.ReopenReason = reason
cycle.ClosedAt = time.Time{}
return s.store.SaveCycle(ctx, cycle)
}
@@ -0,0 +1,150 @@
package payroll
import (
"context"
"fmt"
"github.com/example/payroll-svc/internal/domain/payroll"
)
// BuildPayslip turns one employee's contract and timesheet into a complete
// payslip for the cycle: base pay, overtime, allowances, then deductions and
// tax, in that order. Every amount is in integer cents; nothing here rounds
// until the final net, so a cent never disappears between two lines.
//
// This is the calculation the generated CRUD layer does NOT do — fkit's
// BuildPayslip only copies fields between a DTO and a row.
func (s *Service) BuildPayslip(
ctx context.Context,
cycle payroll.Cycle,
employee payroll.Employee,
timesheet payroll.Timesheet,
) (payroll.Payslip, error) {
if err := ctx.Err(); err != nil {
return payroll.Payslip{}, err
}
if timesheet.EmployeeID != "" && timesheet.EmployeeID != employee.ID {
return payroll.Payslip{}, fmt.Errorf("timesheet/employee mismatch: %s vs %s", timesheet.EmployeeID, employee.ID)
}
slip := payroll.Payslip{
CycleID: cycle.ID,
EmployeeID: employee.ID,
Currency: employee.Contract.Currency,
PeriodFrom: cycle.Start,
PeriodTo: cycle.End,
}
base := s.basePayCents(employee, cycle, timesheet)
slip.Lines = append(slip.Lines, payroll.Line{
Code: "BASE", Kind: payroll.LineEarning, AmountCents: base,
})
if overtime := s.overtimeCents(employee, timesheet); overtime > 0 {
slip.Lines = append(slip.Lines, payroll.Line{
Code: "OT", Kind: payroll.LineEarning, AmountCents: overtime,
})
}
for _, allowance := range employee.Contract.Allowances {
amount := prorateAllowance(allowance, cycle, employee)
if amount == 0 {
continue
}
slip.Lines = append(slip.Lines, payroll.Line{
Code: allowance.Code, Kind: payroll.LineEarning, AmountCents: amount,
})
}
slip.GrossCents = sumKind(slip.Lines, payroll.LineEarning)
for _, d := range employee.Deductions {
amount := d.AmountFor(slip.GrossCents)
if amount == 0 {
continue
}
slip.Lines = append(slip.Lines, payroll.Line{
Code: d.Code, Kind: payroll.LineDeduction, AmountCents: amount,
})
}
tax, err := s.taxCents(employee, slip.GrossCents)
if err != nil {
return payroll.Payslip{}, fmt.Errorf("tax for %s: %w", employee.ID, err)
}
slip.Lines = append(slip.Lines, payroll.Line{
Code: "TAX", Kind: payroll.LineDeduction, AmountCents: tax,
})
slip.DeductionCents = sumKind(slip.Lines, payroll.LineDeduction)
slip.NetCents = slip.GrossCents - slip.DeductionCents
if slip.NetCents < 0 {
slip.NetCents = 0
slip.Underwater = true
}
return slip, nil
}
// basePayCents is the contractual pay for the period: salaried staff get the
// period rate prorated across their contract window, hourly staff get rate ×
// approved units.
func (s *Service) basePayCents(e payroll.Employee, cycle payroll.Cycle, ts payroll.Timesheet) int64 {
switch e.Contract.Kind {
case payroll.ContractSalaried:
full := e.Contract.PeriodRateCents
return prorateSalary(full, e.Contract, cycle)
case payroll.ContractHourly:
return e.Contract.RateCents * int64(ts.Units)
default:
return 0
}
}
// overtimeCents pays approved units above the contractual threshold at the
// contract's overtime multiplier.
func (s *Service) overtimeCents(e payroll.Employee, ts payroll.Timesheet) int64 {
if e.Contract.Kind != payroll.ContractHourly {
return 0
}
threshold := e.Contract.OvertimeThresholdUnits
if threshold <= 0 || ts.Units <= threshold {
return 0
}
extra := int64(ts.Units - threshold)
return int64(float64(e.Contract.RateCents) * e.Contract.OvertimeMultiplier * float64(extra))
}
// taxCents applies the employee's tax band schedule to the gross.
func (s *Service) taxCents(e payroll.Employee, gross int64) (int64, error) {
if len(e.TaxBands) == 0 {
return 0, nil
}
var tax int64
remaining := gross
for _, band := range e.TaxBands {
if remaining <= 0 {
break
}
if band.RateBasisPoints < 0 || band.RateBasisPoints > 10000 {
return 0, fmt.Errorf("invalid band rate %d", band.RateBasisPoints)
}
slice := remaining
if band.UpToCents > 0 && slice > band.UpToCents {
slice = band.UpToCents
}
tax += slice * int64(band.RateBasisPoints) / 10000
remaining -= slice
}
return tax, nil
}
func sumKind(lines []payroll.Line, kind payroll.LineKind) int64 {
var total int64
for _, l := range lines {
if l.Kind == kind {
total += l.AmountCents
}
}
return total
}
@@ -0,0 +1,53 @@
package payroll
import (
"time"
"github.com/example/payroll-svc/internal/domain/payroll"
)
// prorateSalary scales a full period rate down when the contract covers only
// part of the cycle window (a mid-period joiner or leaver).
func prorateSalary(fullCents int64, contract payroll.Contract, cycle payroll.Cycle) int64 {
window := calendarDays(cycle.Start, cycle.End)
if window <= 0 {
return 0
}
covered := calendarDays(laterOf(cycle.Start, contract.StartsOn), earlierOf(cycle.End, contract.EndsOn))
if covered >= window {
return fullCents
}
if covered <= 0 {
return 0
}
return fullCents * int64(covered) / int64(window)
}
// prorateAllowance applies the same window rule to a recurring allowance.
func prorateAllowance(a payroll.Allowance, cycle payroll.Cycle, e payroll.Employee) int64 {
if !a.Prorated {
return a.AmountCents
}
return prorateSalary(a.AmountCents, e.Contract, cycle)
}
func calendarDays(from, to time.Time) int {
if to.Before(from) {
return 0
}
return int(to.Sub(from).Hours()/24) + 1
}
func laterOf(a, b time.Time) time.Time {
if b.IsZero() || a.After(b) {
return a
}
return b
}
func earlierOf(a, b time.Time) time.Time {
if b.IsZero() || a.Before(b) {
return a
}
return b
}