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 |
| Agent re-categorizes the same finding | Yes | category is a hash input — a reclassification is a new identity |
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 — and category reclassifications — create phantom "resolved" + "new" pairs. The old fingerprint appears resolved (not in the new run), and the renamed path or new classification generates a new fingerprint. This inflates new/resolved counts without affecting regression detection. The category case deserves particular attention because taxonomy refinement is something this ecosystem actively does: when an agent's definition is improved and it starts classifying a finding as SEM instead of PRA, that is a fingerprint migration, not a fix plus a discovery.
Empirical Stability
Ecosystem metrics from UluOps Platform API (regression_analysis), 90-day window as of July 2026. Run get_analytics({ metric: "regression_analysis" }) for current data.
Across the ecosystem (90-day window):
| Metric | Value |
|---|---|
| Issues resolved | 5,229 |
| Regressions (resolved → reappeared) | 5 (0.10%) |
| Recurrences (open issue seen again) | 194 |
The near-zero 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. Note the distinction the tracker draws: a regression is a resolved issue reappearing (a fix that didn't hold, or a fingerprint that failed to stay matched), while a recurrence is an open issue observed again across runs — expected behavior, and itself evidence that fingerprints hold stable across runs.
Bounded Impact on Derived Metrics — With One Exception
Fingerprint instability from title wording or path moves affects issue counts (new, resolved, recurring) but not proportional distributions: the failure taxonomy classification (STR/SEM/PRA/EPI) is read from the category field on each occurrence, so those drift sources leave the distribution intact.
category itself is the exception, and it plays a double role worth being precise about. It is simultaneously (a) an input to the fingerprint — part of what makes two findings "the same issue" — and (b) the axis the taxonomy distribution is computed over. A category reclassification therefore moves both: the distribution shifts (correctly — the classification genuinely changed) and the issue's identity breaks (a phantom resolved+new pair enters the counts). An earlier revision of this page claimed the distribution was robust "even if fingerprints drifted" without noting that category-driven drift is precisely the case where identity churn and distribution movement are the same event.
Open measurement question. The rate of category reassignment for otherwise-matched findings (same title, agent, and file path) has not been measured. Until it is, treat resolution and regression statistics as carrying an unquantified contribution from taxonomy refinement — likely small given the near-zero regression rate above, but asserted, not verified. If you are refining an agent's classification behavior deliberately, expect its project's new/resolved counts to inflate during the transition.
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',
});