Fingerprinting

SHA-256 cross-run correlation for tracking issue lifecycles.

Overview

UluOps uses SHA-256 fingerprinting to correlate issues across validation runs. When you run validation today and again tomorrow, fingerprinting determines which issues are new, which are recurring, and which are regressions.

How Fingerprints Work

Each recommendation is fingerprinted by hashing four fields:

text
SHA-256( normalize(title) | normalize(agent) | normalize(filePath) | normalize(category) )

Two recommendations with the same normalized title, agent, file path, and category produce the same fingerprint — even across different runs, timestamps, or projects.

Normalization

Before hashing, each component is normalized to eliminate cosmetic variation:

String fields (title, agent, category):

  • Unicode NFC normalization
  • Lowercase conversion
  • Whitespace collapse (multiple spaces → single space)
  • Leading/trailing whitespace removal
  • Pipe character removal (delimiter escape)

File paths (additional):

  • Backslash → forward slash conversion (Windows/Unix parity)
  • Leading/trailing slash removal
  • Internal path separators preserved

This means "Missing TYPE Annotation" and "missing type annotation" produce identical fingerprints. So do src\file.ts and src/file.ts.

What Changes a Fingerprint

  • Moving code to a different file creates a new fingerprint (different filePath)
  • An agent producing a semantically different title creates a new fingerprint (different title)
  • Running the same agent twice on unchanged code produces identical fingerprints
  • Case, whitespace, and path separator differences are normalized away

Issue Lifecycle States

When a run is saved, each recommendation's fingerprint is matched against existing issues in the project:

StateConditionWhat It Means
NewFingerprint not seen beforeFirst occurrence of this issue
RecurringFingerprint exists and issue is openKnown issue, still present
RegressionFingerprint exists but issue was resolvedPreviously fixed, reappeared
ResolvedPrevious fingerprint not in current runIssue no longer detected

Correlation in Practice

When you call runs.save(), the response includes correlation data:

typescript
const result = await client.runs.save({
  project: 'my-project',
  workflowType: 'post-implementation',
  agents: [{ name: 'code-validator', score: 82, decision: 'PASS' }],
  recommendations: [
    { agent: 'code-validator', title: 'Missing error handling', priority: 'suggested', filePath: 'src/api.ts' },
    { agent: 'code-validator', title: 'Unused import', priority: 'backlog', filePath: 'src/utils.ts' },
  ],
});

console.log(result.correlation);
// { newIssues: 1, recurring: 1, regressions: 0, resolved: 3 }

Comparing Runs

Use diff_runs to compare two runs side by side:

typescript
const diff = await client.runs.diff({
  project: 'my-project',
  baseRun: 3,
  compareRun: 5,
});
// diff.fixed    — issues in baseRun but not compareRun (resolved)
// diff.new      — issues in compareRun but not baseRun (new)
// diff.unchanged — issues in both runs (recurring)

Cross-Phase Deduplication

Multiple agents can flag the same file and line. When two agents produce different titles for the same underlying issue, they generate different fingerprints and track as separate issues. This is by design — each agent's perspective is tracked independently.

Stability and Known Limitations

Fingerprinting solves a hard problem: creating deterministic identity from non-deterministic AI outputs. The normalization layer handles cosmetic variation, but there are structural cases where fingerprints intentionally break:

ScenarioFingerprint Changes?Why
Same code, same agent, re-runNoNormalization eliminates cosmetic variation
Case/whitespace differences in titleNoNormalized before hashing
Windows vs Unix path separatorsNoBackslashes converted to forward slashes
File renamed or movedYesDifferent filePath = different identity
Agent produces semantically different titleYesDifferent analytical framing = different finding

The Design Choice

Title-sensitivity is intentional. If an agent produces a meaningfully different title for the same code location, it typically represents a different analytical angle. Tracking them separately preserves analytical fidelity — collapsing them would lose signal about how agents interpret code over time.

The tradeoff: file renames create phantom "resolved" + "new" pairs. The old fingerprint appears resolved (not in the new run), and the renamed path generates a new fingerprint. This inflates new/resolved counts without affecting regression detection.

Empirical Stability

Data provenance

Ecosystem metrics from UluOps Platform API (regression_analysis) as of April 2026. Run get_analytics({ metric: "regression_analysis" }) for current data.

Across the ecosystem:

MetricValue
Total resolved issues~6,238
Runs with regressions66 of 554 (11.9%)
Issue-level regression rateLow (most fixes are durable)

The low issue-level regression rate suggests fingerprints are stable in practice — if title drift or path instability were common, we'd see significantly higher churn between "resolved" and "new" states. The 11.9% run-level rate reflects that some runs surface at least one regression, typically from scope changes or reverted fixes rather than fingerprint instability.

Bounded Impact on Derived Metrics

Fingerprint instability affects issue counts (new, resolved, recurring) but not proportional distributions. The failure taxonomy classification (STR/SEM/PRA/EPI) is derived from the category field on each occurrence, not from fingerprint correlation. Even if fingerprints drifted, the relative distribution across domains would remain accurate unless the instability were systematically biased toward one category.

Fingerprint-Based Operations

You can query and update issues by fingerprint instead of ID:

typescript
// Find an issue by its fingerprint
const issue = await client.issues.getByFingerprint(project, fingerprint);

// Update status by fingerprint
await client.issues.updateByFingerprint(project, fingerprint, {
  status: 'completed',
  reason: 'Fixed in PR #42',
});