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:
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:
| State | Condition | What It Means |
|---|---|---|
| New | Fingerprint not seen before | First occurrence of this issue |
| Recurring | Fingerprint exists and issue is open | Known issue, still present |
| Regression | Fingerprint exists but issue was resolved | Previously fixed, reappeared |
| Resolved | Previous fingerprint not in current run | Issue no longer detected |
Correlation in Practice
When you call runs.save(), the response includes correlation data:
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:
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:
| Scenario | Fingerprint Changes? | Why |
|---|---|---|
| Same code, same agent, re-run | No | Normalization eliminates cosmetic variation |
| Case/whitespace differences in title | No | Normalized before hashing |
| Windows vs Unix path separators | No | Backslashes converted to forward slashes |
| File renamed or moved | Yes | Different filePath = different identity |
| Agent produces semantically different title | Yes | Different 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
Ecosystem metrics from UluOps Platform API (regression_analysis) as of April 2026. Run get_analytics({ metric: "regression_analysis" }) for current data.
Across the ecosystem:
| Metric | Value |
|---|---|
| Total resolved issues | ~6,238 |
| Runs with regressions | 66 of 554 (11.9%) |
| Issue-level regression rate | Low (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:
// 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',
});