Skip to content

feat(core): give each finding a fingerprint that survives a move - #299

Draft
jedzill4 wants to merge 3 commits into
feat/review-flow-05-standard-risk-reviewfrom
feat/review-flow-06-finding-fingerprint
Draft

jedzill4 wants to merge 3 commits into
feat/review-flow-05-standard-risk-reviewfrom
feat/review-flow-06-finding-fingerprint

Conversation

@jedzill4

@jedzill4 jedzill4 commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Each finding of botica lint gets the fingerprint of ADR 0019 decision 5, and the json and sarif outputs carry it.

FingerprintKey::new(BOTICA_HISTORY_KEY)         # blake3::derive_key("botica history fingerprint v1", value)
  stamp(finding)
    path empty (a change, a message)  -> hash(slug, "", message)
    file_level or no text             -> hash(slug, path, "", "", "")
    otherwise                         -> hash(slug, path, above, line, below)
  # each field preceded by its length as u64 LE; first 16 hex digits of the keyed blake3
  # path from the root of the project; lines trimmed, runs of whitespace -> one space;
  # above/below = nearest non-blank line; no line number
 json:  {"rule", "code", "standard", "severity", "path", "range", "message",
+        "fingerprint": "fc6c37b1e6a10db3" | null}
 sarif: result {
+        "partialFingerprints": {"boticaFinding/v1": "fc6c37b1e6a10db3"}}
  • crates/botica-core/src/fingerprint.rs: FingerprintKey (key, hash, stamp). blake3 stops being optional in botica-core.
  • crates/botica-cli/src/lint_cmd/fingerprints.rs: stamps the final findings, only for json and sarif. The text is the one the run linted. A path that the run did not read (--judged) is read from the index with --staged and from disk otherwise.
  • BOTICA_HISTORY_KEY is removed from the environment of extension processes (ExtensionHost::spawn_with and botica ext sync).

Contract change: the json output gains the key fingerprint (schema_version stays 1, since a key is added and none changes meaning), and the sarif output gains partialFingerprints. The golden tests json_carries_the_schema_version_… and sarif_lists_the_rules_… of crates/botica-core/tests/report.rs were updated. .abide/rubric.json is regenerated because it lists the new fingerprint.rs.

Related issues

Issue 06 of the review flow (fingerprint per finding). Design: ADR 0019 decision 5 (#297). Stacked on #298.

Differences between the issue and the ADR, where the ADR wins:

  • The issue says "hash de regla, ruta y texto normalizado de la línea y sus vecinas". The ADR adds the key (BOTICA_HISTORY_KEY), the length prefix of each field, and that the neighbors are the nearest non-blank lines, and those are what this PR does.
  • A finding with no line text hashes three fields (slug, path, message): the ADR says that the message "takes the place of the three lines". The fixed test vector 7d68e2a43cb6636a pins this reading.
  • The role findings (review:<role>, title as message) are not here: --reviewed does not exist yet.

Evidence

  • Before: botica lint --output-format json has no fingerprint; nothing pairs a finding of one run with the same finding after the lines above it moved.
  • After:
    • cargo test -p botica-cli --test fingerprint: 11 passed, among them a_finding_that_moves_down_keeps_its_fingerprint (the issue's test, through the binary), an_edit_of_the_line_gives_a_new_fingerprint, sarif_carries_the_same_fingerprint_as_json, the_path_is_taken_from_the_root_of_the_project_whatever_the_working_directory, a_staged_run_hashes_the_staged_text_and_not_the_working_tree, a_judged_finding_of_a_staged_run_hashes_the_staged_text_… (fails with the staged read turned off), an_extension_process_does_not_inherit_the_key.
    • cargo test -p botica-core --test fingerprint: 13 passed, with fixed vectors computed apart with the blake3 package of Python (.tmp/vectors.py, not committed): unkeyed line fc6c37b1e6a10db3, keyed line 2f92438af2df38b3, whole file 3859838336ed4b85, message 7d68e2a43cb6636a.
    • cargo fmt --all, cargo clippy --workspace --all-targets --locked -- -D warnings: clean. cargo test --workspace --locked --no-fail-fast: no failure.
    • prek run --files <changed files>: passed. botica lint reports file-size-guard warnings on lint_cmd.rs, eager_test.rs and botica-lsp/tests/common/mod.rs, which were already over 400 lines before this change.
    • Performance role: on 3000 files and 37,823 findings, json with fingerprints 0.65–0.69 s against 0.56–0.59 s for text, which includes writing 21 MB of json.

Review panel

Two rounds of review-panel (spec, security, tests, architecture, contracts, performance, then refute). Neither round had a P0 or P1.

Fixed:

  • Round 1, P2 (architecture-1, contracts-3): --judged --staged hashed the working tree and not the staged text.
  • Round 1, P2 (tests-1): no test read the fingerprint of a --judged run.
  • Round 1, P3 (security-2): botica ext sync passed the key to the process that it starts.
  • Round 2, P2 (tests-1): no test covered a line 0 or past the end of the text.
  • Round 2, P3 (tests-2): no test covered the staged text outside --judged.

Left, with their severity:

  • P2 (contracts-1): two findings of one rule on one line share a fingerprint. ADR 0019 tells them apart with occurrence, a key of the history line that the writer PR adds. SARIF consumers that match by partialFingerprints will merge them.
  • P2 (contracts-2): the output does not say which key made the fingerprints (key_id). It belongs to the run line of the history writer.
  • P3 (security-1): without BOTICA_HISTORY_KEY, json and sarif carry unkeyed fingerprints, which can confirm a guessed secret of low entropy on a flagged line. ADR 0019 decision 5 accepts this when the variable is unset.
  • P3 (tests-3): no test covers that botica ext sync drops the key.
  • P3 (contracts-3): the manual says one line about the fingerprint. A JSON schema of the output is "Not decided" in ADR 0019.
  • P3 (spec-1, spec-2, spec-3): three fields for a finding with no text; no fingerprint when the key is short; compiledAt/compiledBy of the regenerated rubric changed.
  • P3 (architecture): FingerprintKey::from_env is the one read of the environment in botica-core; the variable name lives in botica-ext because both crates need it and botica-ext cannot depend on botica-core; fingerprints.rs borrows baseline::from_root; "fingerprint" also names the key of the result cache, and GLOSSARY.md defines neither; Read::Messages calls of_message itself.

Decisions taken

  • The fingerprint is in the json and sarif outputs only; text, github and markdown stay as they are.
  • fingerprint is always present in json, null when a finding has none, as code and standard are. schema_version stays 1.
  • The SARIF key is boticaFinding/v1, versioned as SARIF 3.27.17 asks.
  • A BOTICA_HISTORY_KEY shorter than 32 bytes leaves every fingerprint out with a warning on stderr, and the exit code stays as it was. ADR 0019 calls it a history that cannot be written; there is no history yet, so the output drops the field.
  • A finding whose line is outside the text, or whose file cannot be read, gets the fingerprint of a finding about the whole file.

Merge Danger

Door: two-way. The output gains keys; nothing is removed or renamed.

Blast Radius: consumers of the json and sarif outputs (a strict reader of the json that rejects unknown keys), and GitHub code scanning, which reads partialFingerprints to match alerts from one upload to the next. Extensions that read BOTICA_HISTORY_KEY from their environment no longer see it.

Rollback: revert the PR. No stored state changes; the result cache keeps diagnostics with fingerprint skipped when it is None.


Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.

A finding carries the keyed blake3 of its rule, its path from the root of the
project and the normalized text of its line and its nearest non-blank
neighbors, as ADR 0019 decision 5 sets. The line number is not an input, so the
fingerprint stays when the lines above the finding move. A finding about a whole
file hashes three empty lines; a finding about a whole change or a message
hashes its normalized message.

The json output gains the key `fingerprint` on each diagnostic, and the sarif
output `partialFingerprints` with `boticaFinding/v1`. BOTICA_HISTORY_KEY keys the
hash; a value shorter than 32 bytes leaves the fingerprints out with a warning,
and extension processes no longer inherit the variable.
…rom ext sync

With `--judged --staged`, the fingerprint of a finding now reads the file from
the index, as ADR 0019 decision 5 sets, and not from the working tree.
`botica ext sync` no longer passes BOTICA_HISTORY_KEY to the process it starts.
A line 0 or past the end of the text gives the fingerprint of a finding about
the whole file, and a `--staged` run hashes the staged text and not the
working tree.
@github-actions

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@jedzill4
jedzill4 added this pull request to stack #295 November 11, 2026 06:35

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.

1 participant