Son of Anton Docs
Code review

Finding Evolution Across Pushes

How findings are fingerprinted, deduped and classified as new, persisting, modified or resolved when the PR head changes.

The engine in son-of-anton-review (feat/cloudflare-native) compares findings with the prior-review snapshots supplied to the run, not an unlimited history of every review. It dedupes findings and records findingEvolution on the run. The model finding schema has a file path but no line numbers; these identities do not represent inline GitHub comments.

Pipeline

Stable fingerprint

stableFindingFingerprint(finding) returns finding:<sha256 prefix>.

  1. If the finding carries an explicit id (fingerprint, findingFingerprint, ruleId, rule_id or checkName), that id is hashed.
  2. Otherwise the hash covers normalized severity, category, path and title.

Normalization strips code spans, URLs, quotes and accents, lowercases, and drops :line:col suffixes from paths. critical maps to blocker, warning to medium.

findingFallbackKey(finding) is the looser identity: severity, path, and up to 18 sorted semantic tokens from title, body and the last two path segments. Tokens are stemmed and aliased (authorization, permission, access all become auth; lacks, skips, without become missing).

Similarity match

findingsAreSimilar(left, right) is true when any of these hold:

TestRule
FingerprintstableFindingFingerprint equal
Fallback keyfindingFallbackKey equal
Fuzzysame normalized path, severities within one rank of each other, and Jaccard token overlap of titles >= 0.35 or of title+body >= 0.28

Severity order

Ranking and severity compatibility use this order, most severe first:

RankSeverity
0blocker (also critical)
1high
2medium (also warning)
3low
4nit
5info

Unknown severities rank last. The review schema only accepts blocker, high, medium, low from the model (src/codex-review-runner.js, REVIEW_SCHEMA).

Dedupe within one review

dedupeFindings(findings) walks the list once. A finding that is similar to one already kept replaces it only if it is more severe, or equally severe with a longer body. contextIds from both are merged so memory provenance is never lost.

Classification against the prior head

classifyFindingEvolution(previous, next, { unresolvedFindings, unresolvedSources }) dedupes both sides, then for each new finding looks for an exact fingerprint match first and a similarity match second.

StatusMeaning
newNo prior finding matched
persistingMatched and canonical text (severity, category, path, title, body) identical
modifiedMatched but text changed
resolvedPrior finding not matched by anything in the new run
unresolvedPrior finding not matched, but flagged by same-head reconciliation as still open

Each entry records finding, previousFinding, fingerprint and fallbackKey. unresolved entries also carry sourceFindings with the reviewKey and fingerprint of every original report.

The service builds previous from the latest supplied prior review plus same-head concerns recovered by reconciliation. Its outcome recorder separately selects the latest supplied review from a different head and writes one outcome per prior finding. A missing evolution status defaults to resolved, but an explicitly incomplete semantic analysis records unknown rather than fixed. That distinction matters: evolution status alone is not proof of a code fix.

Model-reported dispositions

For same-head reconciliation, the model can explicitly disposition an old finding through priorFindingDispositions, a required array in the review schema. Across different heads, the classifier also calls unmatched prior findings resolved; it does not require a disposition for every cross-head omission.

{
  "priorReviewKey": "<exact source review key>",
  "fingerprint": "finding:...",
  "disposition": "resolved | false_positive | no_longer_applicable",
  "explanation": "why",
  "evidence": [{ "headSha": "<current head>", "path": "src/x.js", "quote": "verbatim code" }]
}

The prompt tells the model that a missing finding on an unchanged head is not a fix, that omission is not a disposition, and to return [] when nothing is justified.

Two gates apply before a disposition counts:

  1. The runner rejects any disposition whose evidence is not on the current headSha, has an absolute or .. path, or whose quote is not found in git show <headSha>:<path>.
  2. reconcileSameHeadFindings accepts it only if priorReviewKey and fingerprint name a known source occurrence, the disposition is one of the three allowed values, and the fingerprint is not re-reported in the current findings.

Accepted dispositions are stored as priorFindingDispositions on the review.

validateMemoryFindingProvenance(findings, supplied) rejects any contextIds entry not actually supplied to the model. See Learning, memory and priors.

Same-head reconciliation

reconcileSameHeadFindings(run, findings, dispositions) covers re-runs on the same commit. It collects findings and carried unresolved occurrences from supplied same-head snapshots, applies supported historical dispositions and tracks anything not covered by a current finding or accepted disposition. Each unresolved concern adds a limitation. The runner then forces verdict: needs-attention, caps confidenceScore at 3, sets mergeStatus to caution unless it is already block, and appends limitations to missingValidations. The service performs another continuity pass before publication.

Feedback guard

guardFeedbackResolutions reports omitted prior blocker, high, critical, security/auth/vulnerability-category concerns when supplied feedback has reaction: down linked by context ID or title. It does not reinsert findings or inspect code to decide whether the feedback was the only new signal. The runner turns held concerns into missingValidations and incomplete-context status.

This guard consumes the memory feedback snapshot, not a live reaction attached to every finding. The current publisher creates review bodies rather than inline comments, so do not infer a complete per-finding reaction-learning loop from the guard.

Publishing across heads

GitHubPublisher creates a check run with external_id = reviewKey and updates that check when its ID is known. PR reviews are POSTed with an <!-- anton-review <reviewKey> --> marker and commit_id = headSha. There is no per-finding inline-comment creation or update.

Publication claims are scoped to a review key and action, not to a finding across all heads. A newer head or deliberate variant can publish another review containing a persisting concern. Native uncertain publication requires remote-receipt reconciliation before resending; the local SQLite ledger instead supports lease-based reclamation. Neither establishes a blanket “no duplicate comments across heads” guarantee. See Publish modes.

Known remaining work

The publisher does not edit individual prior findings according to their evolution status. Evolution and outcomes support run history and subsequent review context; omission, similarity matching and feedback are not line-level addressed detection.

Source evidence

  • son-of-anton-review/src/finding-evolution.js:3-4,64-100,143-251,257-400.
  • son-of-anton-review/src/codex-review-runner.js:210-248,2719-2783; src/review-service.js:397-462,478-515.
  • son-of-anton-review/src/github-publisher.js:22-66; src/sqlite-store.js:415-462; cloudflare-native/src/state-transport.js:490-524.

On this page