Skip to content

Keep fingerprints stable across merges, selections and renames - #59

Draft
tauanbinato wants to merge 1 commit into
mainfrom
feat/56-stable-fingerprints
Draft

tauanbinato wants to merge 1 commit into
mainfrom
feat/56-stable-fingerprints

Conversation

@tauanbinato

@tauanbinato tauanbinato commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Closes #56

Fingerprints stay the same across merges, stacked branches, changes to which files a check selects, and renames. A dismissed finding is no longer raised again because main moved underneath it.

What changes

  • Shared logic. A repeat is identified by its copies. Each copy is path::function, sorted, or path#hash for a copy outside a function. The owner path, the statement-window hash and the representative pair no longer count. A baseline accepts a repeat while every copy is one that an accepted repeat names. A copy that goes keeps the repeat accepted; a new copy that joins raises it again.
  • File organization. An outline is identified by its path. A baseline entry accepts it while the Jaccard similarity of member names is at least 0.8.
  • Custom hunks. Each run of changed lines is its own hunk, as a diff without context lines has it. It is shown with up to 3 unchanged lines on each side, never another run's lines. It is identified by the innermost definition JevGate's parser finds around it, plus its changed lines, plus an occurrence suffix when the same change repeats in one definition. Git's hunk header is dropped from the identity but is still sent as the in evidence. Hand-written examples keep each hunk whole.
  • Renames. With --base and in the hook's turns, a finding of a renamed file is also known under its old path (aliases).
  • Function and other content units: unchanged. --base still does not search unchanged files.

Decisions (user, 2026-10-03, recorded on #56)

  1. Transition. An entry still holding a v1 fingerprint keeps accepting its finding through the finding's fingerprint_v1 until a baseline write rewrites it to v2 with its reason and note. The writes are jevgate baseline, --merge, and baseline mark, which rewrites the v1 entries matched by findings of the last report. Gates stay green on upgrade. One whole check plus a write produces the one-time diff.
  2. Outline similarity: Jaccard ≥ 0.8 of member names.
  3. Members: shared-logic and outline entries store members.
  4. Baseline version: the file stays version: 1. Baselines without the new fields load unchanged.

Other consumers after the switch

  • SARIF: partialFingerprints carries both jevgateFingerprint/v1 (the v1 fingerprint where it changed, else the same value) and jevgateFingerprint/v2, so code-scanning alerts keep their identity across the upgrade. v1 is dropped in the release after this one (user, 2026-10-04, recorded on Keep finding fingerprints stable across merges of main or a parent branch #56); this release's notes mark it deprecated.
  • GitLab Code Quality: uses the v2 fingerprint. The first merge request after the upgrade shows changed shared-logic, outline and hunk findings once as resolved and new, because GitLab compares against the base pipeline's report.
  • MCP ids: the finding's v2 fingerprint, for findings and undecided units alike. Ids are read from the current report, so nothing carries over. The tool descriptions now say the path is not part of a shared-logic id.
  • Hook turn memory: a finding counts as already told when its fingerprint or any earlier one (v1, or the pre-rename path) is remembered. A rename or an upgrade within a turn therefore does not re-report it.
  • Hook dismissals: a dismissal is now found per finding. A mark is "this turn's" only when the turn-start baseline did not already accept the finding, through any of its fingerprints. An entry rewritten from v1 to v2 is therefore never reported as the agent's dismissal (tested).
  • changes.rs lineage: persistent and resolved also match through earlier fingerprints. A snapshot taken before the upgrade or rename does not show resolved plus introduced pairs (tested).
  • Baseline guard: still compares entries by fingerprint. A commit carrying the one-time rewrite shows "accepts N more findings, drops N findings" once. Reasons are kept, and the hook's dismissal list stays exact.

Tests and what they protect

  • fingerprints::a_repeat_has_one_fingerprint_whichever_copy_a_check_selects: a check of b.rs with a.rs as context gives the same fingerprint as a whole run. The v1 values differ, which is the bug.
  • fingerprints::a_repeat_a_new_copy_joins_is_asked_about_again_and_one_a_copy_leaves_is_not: new duplication is never hidden, and a removed copy keeps the acceptance.
  • fingerprints::an_old_baseline_accepts_through_earlier_fingerprints_until_a_write_rewrites_it: a hand-written 0.35 baseline (v1 entry, later, note #12) still accepts its finding. mark rewrites it to v2 and keeps the reason and note. The turn's dismissals are only the mark, and the gate passes.
  • fingerprints::writing_the_baseline_over_an_old_one_moves_each_entry_to_its_current_fingerprint: covers baseline with and without --merge. No v1 entry is kept, the reason and note survive, and members is written.
  • organization::an_accepted_outline_stays_accepted_while_its_members_stay_similar: 34 of 35 names shared stays accepted; 34 of 45 is raised again.
  • changed::a_hunk_keeps_its_identity_when_a_change_nearby_or_a_function_above_comes_and_goes, plus hunks::each_run_of_changed_lines_is_a_hunk_with_the_unchanged_lines_around_it: the parent-edit and new-function-above cases from the spike.
  • custom::a_renamed_files_finding_stays_accepted_through_its_old_path: rename aliases.
  • entries::an_outline_stays_similar_until_a_fifth_of_its_names_change: the 0.8 threshold.
  • Extended: the SARIF test (both partial fingerprints), the changes.rs lineage test (earlier fingerprints), and the blank-line hunk test (now two runs).

Validation

  • cargo fmt --check and cargo clippy --locked --all-targets -- -D warnings: clean.
  • cargo test --locked: 898, 83 and 3 passed. rules_test::a_question_that_separates_its_examples_passes_and_a_rerun_asks_nothing failed once in a full run (models read typesafe/jev-1.13), then passed alone and in two later full runs. It looks like a pre-existing parallel-test flake.
  • The gate, jevgate check --base origin/main --rule all --include-tests, with the installed 0.35.0 that CI runs: passes with no new findings.
    • Batch 1: 166 requests, 309,603 tokens, $0.0130.
    • Batch 2: 101 requests, 211,593 tokens, $0.0089.
    • Batch 3: 33 requests, 71,392 tokens, $0.0030.
    • Final: 0 requests (cached).
    • No other live inference.

Findings fixed

  • Split baseline.rs: matching moved to baseline/entries.rs, listing and stats to baseline/listing.rs.
  • Moved the fingerprint helpers to compose/identity.rs.
  • Shared helpers:
    • Finding::fingerprints (lineage and the hook)
    • entries, kept and mark_entries in the baseline
    • sites in duplicates.rs
    • note_renames in the plan
    • enclosing for hunks
    • renamed_only for grouped findings
  • Split known and split into smaller functions.
  • Shared the state map in the lineage test.

Dismissals (one at a time, with the reason)

  • src/baseline.rs:19 file-organization, intended: the baseline file's reads and writes; this PR moved matching and listing out.
  • src/baseline.rs:358 shared-logic, wrong: one read_latest call, optional for a mark and required for a write.
  • src/hook/events.rs:311 told, intended: as on main; one partition condition changed.
  • src/units/compose/comments.rs:44 comment_findings, intended: as on main; only the identity is named and aliased.
  • src/units/compose/mod.rs:667 finding, intended: as on main; it only takes the fingerprint from known().
  • src/units/compose/mod.rs:374 shared-logic, wrong: one first-line expression in three struct literals.
  • src/units/compose/mod.rs:350 shared-logic, wrong: a two-arm match on the custom question in two different outputs.
  • src/units/custom/hunks.rs:189 split, wrong: a short loop over parts that helpers compute.
  • src/units/custom/items.rs:199 shared-logic, wrong: each kind zips its own items with their ids, and the items differ.
  • src/units/outcome/mod.rs:258 rule_outcome, intended: as on main; only a pattern field was added.
  • src/units/plan/mod.rs:98 plan, intended: as on main; only the note_renames call was added.
  • src/units/tests/custom.rs:6 file-organization, intended: the custom-question tests, one file per rule; this PR adds a rename test.
  • src/units/tests/mod.rs:34 file-organization, intended: the unit tests' shared helpers; this PR adds accept_all.

The location marks also re-marked 7 older entries at the same lines. I restored their original reasons and notes, so the baseline diff only adds entries. The dismissals were made with the installed 0.35.0, so they carry the v1 fingerprints CI matches. 0.36 accepts them through fingerprint_v1 until a write rewrites them.

Known limits

  • A rename alias for a repeat maps only the renamed file's own copies. A copy in another file renamed in the same change is not mapped.
  • Two methods with the same name in one file share a member key.
  • Whole-file checks have no rename information, so a baseline written from a whole check after a rename re-accepts under the new path without the old reason. --base checks, the hook and mark keep the reason.
  • COMPOSITION is now v15. Rule versions and the answer cache are unchanged. Custom hunk questions ask about smaller units, so their requests differ from 0.35's.

A repeat is identified by its copies, a file outline by its path with
its members matched by similarity, and a custom hunk by the definition
around it and its own run of changed lines. A finding of a renamed file
is also known under its old path. Baseline entries written before still
accept their findings through the fingerprints they had then, and the
next baseline write rewrites them, keeping reasons and notes. SARIF
carries both fingerprints for a release.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keep finding fingerprints stable across merges of main or a parent branch

1 participant