Phase 4: Call-Site Tracer

The tracer is the final phase. When the classifier identifies a breaking change, the tracer answers the question: "who is affected?". It scans the entire repository to find every file that imports the broken symbol, then counts arguments at each call site to determine if the call is already broken.


Two-tier architecture

The tracer uses a lazy, two-tier scanning strategy designed for speed in large repositories:

1
JIT Scanner (fast pass)

Uses git grep to find all files mentioning the symbol name. No AST parsing — pure text search on the git index. For a 50,000-file repo, this takes approximately 50ms.

|
2
Call-Site Tracer (deep pass)

AST-parses only the files identified by the scanner. Locates actual import statements, resolves aliases, and counts arguments at each call site. Typically 5-15 files per broken symbol.

JIT Scanner

The JIT Scanner is the "fast pass" — it narrows down the entire repo to just the files that reference the broken symbol.

Step 1: Git grep

Runs git grepon the git index at the head ref. This is extremely fast because it operates on git's internal data structures, not the filesystem.

# What the scanner runs internally:
git grep -n --word-regexp 'processPayment' HEAD -- '*.ts' '*.tsx' '*.js' '*.jsx'

# Returns:
# HEAD:src/checkout/handler.ts:18:import { processPayment } from '../payments'
# HEAD:src/invoices/gen.ts:4:import { processPayment } from '@/api/payments'
# HEAD:src/subscriptions/renew.ts:9:processPayment(amount, currency);
# HEAD:src/payments/index.ts:2:export { processPayment } from './core';

Key properties of this approach:

  • Reads committed content — not the working tree. Consistent with git refs
  • Respects .gitignore automatically. node_modules excluded for free
  • Excludes binary files automatically
  • Capped at maxGrepResults (default 500) to prevent runaway scans

Step 2: Import classification

For each grep match, the scanner reads the file content and classifies it using language-specific import pattern detection. Each language provides aLanguageStrategy that knows its import syntax:

// TypeScript import patterns:
import { processPayment } from './payments';      // Named import
import { processPayment as pay } from './payments'; // Aliased import
import * as payments from './payments';             // Namespace import
const { processPayment } = require('./payments');   // CJS require
const pay = await import('./payments');              // Dynamic import

// Python import patterns:
from payments import process_payment
from payments import process_payment as pay
import payments  # then: payments.process_payment()

// Go import patterns:
import "project/payments"  // then: payments.ProcessPayment()

// Java import patterns:
import com.project.payments.ProcessPayment;
import static com.project.payments.ProcessPayment;

// Rust import patterns:
use crate::payments::process_payment;
use crate::payments::{process_payment, other_fn};

Step 3: Barrel file walking

If a file re-exports the symbol (a barrel file), the scanner adds it to a BFS queue and scans its consumers recursively.

// src/payments/index.ts (barrel file)
export { processPayment } from './core';

// src/checkout/handler.ts imports from the barrel
import { processPayment } from '../payments';
// The scanner traces: handler.ts → payments/index.ts → payments/core.ts

Cycle detection prevents infinite loops in circular re-exports. A visitedSet tracks every file the scanner has seen. Depth is capped atmaxBarrelDepth (default 10) for deeply nested barrel architectures.

Call-Site Tracer

After the scanner identifies importer files, the Call-Site Tracer AST-parses each one and locates call expressions:

// For each confirmed importer file:
// 1. Parse the file into an AST
// 2. Find all call expressions matching the symbol name
// 3. Count arguments at each call site
// 4. Compare against the new signature's required parameter count

// Result per call site:
{
  file: "src/checkout/handler.ts",
  line: 18,
  argumentCount: 3,
  status: "broken"  // provides 3 args, new signature needs max 2
}

Tracer output

The tracer populates the callers array on each FunctionChange:

core/types.ts
interface CallerInfo {
  file: string;            // "src/checkout/handler.ts"
  line: number;            // 18
  column: number;          // 4
  argumentCount: number;   // 3
  importType: string;      // "named", "namespace", "default"
  localName: string;       // "processPayment" or "pay" (if aliased)
  status: 'broken' | 'ok' | 'indeterminate';
}

Terminal output

The reporter formats tracer results with status indicators:

  Affected call sites (3):
    X  src/checkout/handler.ts:18 -- provides 3 arg(s), needs max 2
    X  src/invoices/gen.ts:31 -- provides 3 arg(s), needs max 2
    .  src/subscriptions/renew.ts:9 -- 2 arg(s), OK
  • X — broken: argument count does not match the new signature
  • . — ok: argument count satisfies the new signature
  • ? — indeterminate: call uses spread or computed arguments

Performance characteristics

OperationTypical TimeBound
git grep on 50K-file repo~50msO(repo size)
Import regex on 15 files~2msO(grep matches)
Barrel BFS (3 levels)~20msO(barrel depth x width)
AST parse + call count~5ms/fileO(importer count)
Total Phase 4< 200ms

Configuration

OptionDefaultEffect
enableTracertrueDisable entirely for faster CI runs
maxGrepResults500Cap grep output for common symbol names
maxBarrelDepth10Prevent runaway barrel chains
maxTracerFiles100Cap AST-parsed files per symbol