Skip to content

SARIF evidence chains + external-findings fusion (ingest-sarif, overlap gate, MCP tool)#83

Merged
emrecdr merged 26 commits into
mainfrom
feat/sarif-fusion
Jul 10, 2026
Merged

SARIF evidence chains + external-findings fusion (ingest-sarif, overlap gate, MCP tool)#83
emrecdr merged 26 commits into
mainfrom
feat/sarif-fusion

Conversation

@emrecdr

@emrecdr emrecdr commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Summary

This branch ships two SARIF features.

Evidence chains: codelore check --format sarif and codelore diff --format sarif now attach per-finding commit lineage (top-5/check, top-3/diff, newest-first, rename-aware via the canonical-lineage fact store) as SARIF codeFlows + relatedLocations, with versioned partialFingerprints (gateFinding/v1, primaryLocationLineHash) for stable GitHub Code Scanning identity; --format is a typed clap ValueEnum and verdict lines stay on stderr with exit codes unchanged.

Ingest-SARIF fusion: codelore ingest-sarif parses external scanner SARIF (CodeQL/Semgrep/clippy dialect variance handled via documented fallback chains) into a prune-safe per-repo sidecar (external-findings.duckdb-ext, (engine,fingerprint) PK, transactional per-engine replace); the new finding-hotspot-overlap analysis joins findings × revision-percentile (SQL-equivalent tie ranks) × code-health band into act-now/plan/note priorities; the max_findings_in_hot_files gate enforces an act-now ceiling in check (row-reuse, skip-with-ledger-record when the sidecar is absent or empty, never creates the sidecar on read); an eighth MCP tool exposes the same table.

~1,900 lines of new tests cover dialect parsing, store lifecycle, percentile semantics, fingerprint stability, and end-to-end CLI/MCP contracts.

Verification

  • Per-task spec + quality reviews on every task, with review-ordered fixes re-reviewed to approval
  • Whole-branch review (cross-task integration, docs truthfulness, constraint sweep) — 5 findings fixed and independently re-verified READY TO MERGE
  • just ci (fmt + clippy -D warnings + cargo-deny + full workspace tests) green at HEAD

Known debt (deliberately shipped, tracked)

  • Sidecar readers open read-write (DuckDB single-writer lock; recommend read-only access mode follow-up)
  • check --ratchet --format sarif combination emits no SARIF (ratchet paths return before emission)
  • Evidence-query errors degrade silently to chainless results (a tracing::warn! would help)
  • Diff results carry no partialFingerprints (check results carry two) — asymmetric GitHub dedup
  • analyze_head_at_rev duplicates ~32 lines of analyze_at_rev (refactor candidate)

Introduce `codelore_lib::external` with a SARIF 2.1.0 subset parser
(`parse_sarif`) that normalises findings from Semgrep, Clippy, and
CodeQL dialects into a uniform `ExternalFinding` struct.

Key design points:
- Rule-id: `result.ruleId` else `rules[ruleIndex].id` (CodeQL pattern)
- Level fallback: result → rule defaultConfiguration → "warning"
- Fingerprint chain: partialFingerprints.primaryLocationLineHash →
  any partialFingerprints value → any fingerprints value →
  sha256(engine|rule_id|path|start_line) self-hash
- Path normalisation: strip `file://` scheme, strip leading `./`
- Unparseable individual results are skipped with tracing::warn;
  non-SARIF documents return CodeLoreError::Analysis

Three handwritten fixtures (semgrep / clippy / CodeQL) exercise each
dialect quirk; 7 table-driven tests all pass.
Introduce `quality_gates::evidence` with `EvidenceCommit` + `evidence_for_path`.
The function returns the top-N commits (newest-first, date DESC / rev DESC for
tie-breaking) that touched a given path, joining `changes`/`changes_lineage`
with `commits` for author, churn (loc_added + loc_deleted), and a SQL-truncated
first-line message (DuckDB substr counts code points, not bytes — no Rust slicing
needed). A nonexistent path returns an empty Vec, not an error.

Four tests on `biomarker_repo`: newest-first ordering, churn > 0, n-limiting,
and deterministic ordering across identical calls. All 4 pass.
Add `codelore ingest-sarif --repo . file1.sarif [file2…]` that parses
each SARIF file via the B1 parser, groups findings by engine, and calls
`ExternalStore::replace_engine` per engine so re-ingest is idempotent.
Prints `ingested N finding(s) from M engine(s) → <path>` on success.

The sidecar lives at `<cache_root>/codelore/<repo_hash_8>/external-findings.duckdb-ext`.
The `.duckdb-ext` extension was chosen after verifying in cache.rs that
the LRU pruner filters on `.extension() == "duckdb"` — `.duckdb-ext`
returns `"duckdb-ext"` from that call and is skipped (see B2 report).

Also extracts `pub fn repo_cache_dir(cache_root, repo_path)` from
`cache.rs` so the SHA-256 repo-dir derivation is defined once and called
from both `quality_gates::ledger::ledger_dir` and the new `ExternalStore`.
Ledger tests remain green after the extraction.

5 store tests: fixture ingestion counts, re-ingest idempotency, two
engines coexist.
Add `codelore check --format sarif` — a SARIF 2.1.0 document for all gate
violations, with evidence chains for per-file breaches.

Key design:
- `write_check_sarif(violations, evidence, repo_root, head_sha, w)` in
  output/sarif.rs. One ReportingDescriptor per distinct gate (deduped via
  BTreeSet for stable ordering). One result per GateViolation.
- Dual partialFingerprints per result: `gateFinding/v1`
  (sha256(gate|path|head_sha) — versioned per SARIF spec SHALL) and
  `primaryLocationLineHash` (sha256(repo_root|path) — mirrors the hotspots
  emitter convention GitHub deduplicates on).
- Per-file violations get `relatedLocations` + one `codeFlow`/`threadFlow`
  with ≤5 evidence commits (newest-first from evidence_for_path). Repo-wide
  paths ("(repo-wide)", "(degraded)") emit uri "." with no evidence chain.
- Verdict lines (FAIL / WARNING / PASS) always go to stderr; exit codes
  unchanged. GHA annotations suppressed in sarif mode (stdout is JSON only).

Evidence collection: after evaluate_all_gates, for each violated per-file
path (deduped) call evidence_for_path(db, opts, path, 5) via unwrap_or_default
(graceful empty on DB errors — evidence is best-effort context, not gating).

Tests:
- `check_sarif_structure_and_fingerprints` — 2 violations + 1 evidence
  commit; asserts version, rules dedup, both fingerprint keys, codeFlows
  nesting, repo-wide result has no codeFlow.
- `check_sarif_fingerprint_stability` — pinned sha256 regression guard for
  gateFinding/v1; any accidental hash-input change fails loudly.
  Pinned: sha256:88a397f9502c73ea613a9cabc2d9d20fb6ecfa80c1acd93e00438669156a1d0b
- CLI smoke: `check --format sarif` on biomarker_repo with code_health_min=100.0
  → exit 1 AND stdout parses as SARIF with ≥1 result AND "FAIL" on stderr.
… test

Two B1-review folds into the B2 commit:

1. sarif_parse.rs::normalize_path docstring was wrong: claimed
   file:///abs/path → src/main.rs (stripped to repo-relative). The
   function only strips the scheme; absolute paths arrive with their
   leading slash intact (e.g. /home/runner/work/repo/src/Foo.java).
   Docstring now states the real contract; store.rs gains a module-level
   note explaining that absolute paths are stored as-is and will
   intentionally not match repo-relative hotspot paths in B3.

2. Added multi_run_sarif_produces_findings_from_both_engines: a single
   SARIF document with two runs (engine-a: 1 finding, engine-b: 2
   findings) parses to 3 total; grouping by engine and calling
   replace_engine per engine stores all 3. Exercises the multi-run
   loop that was previously untested.
Add two tests required by the A1 review verdict:

1. evidence_message_head_truncated_to_80_ascii — builds a one-commit
   fixture repo with a 95-char ASCII message, ingests it, and asserts
   message_head.chars().count() == 80. Verifies the SQL
   substr(split_part(..., chr(10), 1), 1, 80) cap is enforced.

2. evidence_message_head_multibyte_safe — 79 ASCII chars + 5 CJK
   ideographs (3 UTF-8 bytes each, 84 code points total). Asserts
   message_head has ≤ 80 code points, starts with the 79 ASCII chars,
   and does not panic. Confirms DuckDB's code-point-aware substr never
   splits at a byte boundary mid-ideograph.
…ral signal

Adds the `finding-hotspot-overlap` analysis (B3): joins the external
SARIF sidecar (populated by `codelore ingest-sarif`) with hotspot and
code-health behavioral signals to produce a per-path priority triage.

Each output row carries: path, findings count, comma-joined engines,
worst severity level, hotspot_score, revs_percentile (PERCENT_RANK of
revision count computed in Rust), health_band, and priority label
("act-now" | "plan" | "note"). Sort: priority asc, findings desc,
path asc.

Join strategy: the external sidecar and FactsDb each own a separate
DuckDB connection (both are !Send + !Sync); no ATTACH is performed —
all joining happens in Rust via HashMaps.

Touches (7-touch §TEMPLATE):
- analysis.rs: FindingHotspotOverlap variant + as_str() + registry!
- analyses/mod.rs: pub mod finding_hotspot_overlap
- analyses/finding_hotspot_overlap.rs: full implementation + 8 unit tests
- external/store.rs: findings_by_path() + PathFindings struct
- output/csv.rs: write_finding_hotspot_overlap_csv()
- output/markdown.rs: write_finding_hotspot_overlap_markdown()
- cli_api.rs: re-export `external` module
- main.rs: FindingHotspotOverlap dispatch arm + dispatch fn
- cli_test.rs: "finding-hotspot-overlap" added to EXPLAIN_UNCOVERED
- Cargo.toml: [[test]] finding_hotspot_overlap_test
- tests/finding_hotspot_overlap_test.rs: 4 integration tests
Fix 1 — replace_engine atomicity:
The DELETE + INSERT loop in replace_engine was non-transactional: a
process kill after DELETE but before all INSERTs completed left the
engine's findings permanently deleted. Wrapped the entire operation in
conn.unchecked_transaction() (takes &self, rolls back on drop) so any
mid-loop failure is automatically rolled back.

Fix 2 — multi-file same-engine grouping test:
Adds multi_file_same_engine_combined_count_is_sum to external_store_test:
two batches from the same engine (simulating two SARIF files) are
combined before calling replace_engine once. Asserts count = 3 (sum of
both files), not 2 (file2-overwrites-file1). This pins the sharpest
correctness property of the per-engine replace design.
- Promote `SARIF_SCHEMA` / `CODELORE_HOMEPAGE` in output/sarif.rs to
  `pub const SARIF_SCHEMA_URL` / `pub const TOOL_INFO_URI` so all four
  in-crate emitters and the diff emitter share one source of truth.

- diff_output::emit_sarif gains `db_opts: Option<(&FactsDb, &Options)>`;
  when `Some`, evidence_for_path(n=3) is called per CODELORE-HOTSPOT,
  CODELORE-CLONE and CODELORE-DELTA-HEALTH finding and the commits are
  serialised as SARIF codeFlows → threadFlows → locations chains.

- Degrading delta-health functions emit one CODELORE-DELTA-HEALTH result
  per unique path (Outcome::Bad, deduplicated); rule declaration is
  conditional on delta_health being present.

- diff.rs: new analyze_head_at_rev returns (RevAnalyses, FactsDb, Options)
  keeping the in-memory db alive after the worktree is dropped; run_diff
  returns Result<(DiffOutput, FactsDb, Options)>; run_diff_cmd
  destructures and passes Some((&head_db, &head_opts)) to emit.

- Stale URLs fixed: $schema was schemastore.azurewebsites.net, informationUri
  was github.com/emre/codescene — both now reference the constants.

- Test: diff_sarif_schema_url_and_info_uri_use_canonical_constants asserts
  $schema == SARIF_SCHEMA_URL, informationUri == TOOL_INFO_URI, and the
  degrading CODELORE-DELTA-HEALTH result for the monster function carries
  ≥1 codeFlow with a populated threadFlow.

- finding-hotspot-overlap added to EXPLAIN_UNCOVERED allowlist in
  cli_test.rs (no explain topic wired yet; decision recorded in-list).
Adds the `max_findings_in_hot_files` optional gate in `[gates]`:

  [gates]
  max_findings_in_hot_files = 0  # zero act-now overlap rows allowed

Fails when the number of "act-now" rows from `finding-hotspot-overlap`
(external findings that sit in a high-percentile hotspot with a red
code-health band) exceeds the threshold.

**Skip semantics**: the gate is skipped — not failed — when the
external findings sidecar is absent or empty (the user has not yet run
`codelore ingest-sarif`). A skip prints a distinct warning line and
records verdict="skipped" in the ledger; it does not affect the exit
code. Mirrors the degraded print style.

Touches:
- quality_gates/mod.rs: Gates.max_findings_in_hot_files + is_empty() +
  evaluate_finding_overlap_rows() pure fn + 6 unit tests
- quality_gates/ledger.rs: extend verdict rustdoc to include "skipped"
- main.rs: wire gate inline in run_check_cmd after evaluate_all_gates;
  violations is now mut to allow extending from the overlap gate
- docs/advanced-usage.md: one-sentence §11.8 entry
…e path

Extracts the per-engine grouping logic from run_ingest_sarif_cmd into a
pure lib fn external::group_findings_by_engine(Vec<ExternalFinding>)
-> HashMap<String, Vec<ExternalFinding>>.

The command now builds a flat Vec across all input files then calls the
lib fn — making the grouping step independently testable.

Adds group_findings_by_engine_combines_two_fixture_files to
external_store_test: parses the semgrep + clippy fixtures into a flat
vec, calls group_findings_by_engine, ingests per engine, asserts count=4
across 2 engine keys. This pins the real ingest code path, not just the
store contract.
Convert CheckArgs.format from String to a CheckFormat ValueEnum
(Text | Sarif) matching the existing DiffFormat pattern. Unknown
values like --format sariff now fail at parse time with a clap
error rather than silently falling back to text.

Also drop the needless evidence_locs.clone() in sarif.rs (build
code_flow_locs via .iter() before consuming evidence_locs into
relatedLocations) and fix two B-lane clippy regressions in
finding_hotspot_overlap.rs (doc backticks for PERCENT_RANK,
#[allow(dead_code)] on compute_percent_ranks).

New test: check_default_format_is_text_not_json — verifies that
omitting --format yields text output, not a JSON document.
Note: --format sarif on a PASS emits a zero-result SARIF document
(valid; the caller decides whether an empty result set matters).
Splits the condition introduced in the overlap gate commit so it
passes `cargo fmt --all --check` at the CI-exact level.
Document check --format sarif (stdout SARIF, stderr verdicts, exit codes
unchanged; zero-result document on PASS), the commit evidence chain
(lineage-aware, newest-first), fingerprint keys (gateFinding/v1 for
identity, primaryLocationLineHash for GitHub dedup), codeFlows +
relatedLocations GitHub rendering, and the GitHub Actions upload-sarif
snippet with continue-on-error note.

Document diff --format sarif evidence chains (up to 3 commits per file).

README: one-sentence add on --format sarif with chain callout.

CHANGELOG [Unreleased]:
- Added: check --format sarif with evidence chains
- Added: diff --format sarif evidence chains
- Added: ingest-sarif subcommand and external findings sidecar
- Added: finding-hotspot-overlap analysis (behavioral x static)
- Added: max_findings_in_hot_files gate
- Fixed: stale schema URL and informationUri in diff SARIF emitter
Add relatedLocations alongside codeFlows on all evidence-bearing diff
SARIF results (CODELORE-HOTSPOT, CODELORE-CLONE, CODELORE-DELTA-HEALTH).
GitHub's inline annotation panel reads relatedLocations; codeFlows feeds
the "Show paths" flows tab — both are required for full GitHub rendering.

Refactor code_flow_for closure into evidence_for returning
Option<(codeFlows, relatedLocations)>: relatedLocations entries are plain
location objects (no wrapper); codeFlows threadFlowLocations wrap each in
{"location": …} as required by SARIF §3.36. Remove the "module" field from
threadFlowLocations — it means code module/namespace in SARIF, not commit
message; message_head is already in location.message.text.

Tests:
- Extend diff_sarif_schema_url_and_info_uri_use_canonical_constants to
  also assert relatedLocations on the delta-health result (non-empty;
  plain physicalLocation without "location" wrapper).
- Add diff_sarif_hotspot_rank_entrant_carries_code_flows_and_related_locations:
  fixture with no Rust files at base and src/hot.rs changed twice at head,
  guaranteeing a CODELORE-HOTSPOT rank-entrant; asserts both codeFlows and
  relatedLocations are present and non-empty, and no stray "module" key.
Add finding_hotspot_overlap MCP tool (8th tool) to the stdio server.
The tool opens the sidecar only when it exists; an absent or empty
sidecar returns {"findings":[], "note":"run codelore ingest-sarif first"}
rather than an error, so agents calling it on a fresh repo get a
structured hint instead of a crash.

mcp_test: count 7→8, finding_hotspot_overlap in names list, new
mcp_finding_hotspot_overlap_returns_note_when_sidecar_absent test
(tiny_repo has no sidecar → note path exercised).

docs/advanced-usage.md: replaces the one-line overlap-gate note with
a full behavioral×static fusion section covering ingest-sarif usage,
dialect notes (Semgrep/clippy/CodeQL variance from §VALIDATED),
finding-hotspot-overlap column reference + priority rules, gate config
example, and finding_hotspot_overlap MCP tool entry.

README: Behavioral×static fusion paragraph with the ingest-sarif →
overlap → gate → MCP tool pipeline.

CHANGELOG: MCP entry updated from seven to eight tools.
Removes .superpowers/sdd2/task-B4-report.md from tracking (file stays
on disk) and git-ignores the .superpowers/ scratch directory so
session ledgers and task reports can no longer ride into commits.
… emit vacuous SARIF

Four external-findings correctness fixes in the check/analyze/ingest paths:

- Empty-but-present sidecar now takes the same skip path as an absent one
  in the max_findings_in_hot_files gate. Previously a present-but-empty
  store made run_finding_hotspot_overlap_with return Err, aborting the
  whole check with exit 4 — contradicting the documented skip contract.
  The Some(store) arm now checks the row count and joins the None case
  when zero, mirroring the MCP overlap tool.

- analyze --analysis finding-hotspot-overlap now opens the sidecar under
  --cache-dir instead of the hardcoded default XDG root, matching where
  the FactsDb and ingest-sarif read/write.

- Re-ingesting a clean (zero-result) SARIF scan now clears that engine's
  stale rows. parse_sarif discards engine names of zero-result runs, so
  the grouping step never saw them; a new parse_sarif_engines companion
  surfaces every engine present, and ingest seeds empty batches for them
  so replace_engine drops the prior run's findings. Restores the
  IngestSarif contract that the stored count is always the current run.

- check --format sarif on a repo with no thresholds now emits a valid
  zero-result SARIF document instead of printing nothing, so the
  documented upload-sarif pipeline no longer breaks on a vacuous pass.
Doc- and comment-only truth fixes, no behavior change:

- Reword task/plan-ID references to state the current contract: drop the
  §VALIDATED, per-R7, B3, B1, and A3 tags in favor of the fact each was
  standing in for (SARIF 2.1.0 verified, connection-isolation rationale,
  the overlap join, the dialect fixtures, the schema-constant check).

- advanced-usage.md §11.8: the SARIF evidence entry shows date, author,
  message head, and churn — not the full commit SHA — and the chain is
  ordered by recency (commit date, then revision), not by how heavily a
  file was touched. Correct the description to match the emitter.

- advanced-usage.md + mcp.rs check_gates: the MCP tool evaluates a subset
  of codelore check (no max_findings_in_hot_files, no degraded/ratchet),
  so its verdict can diverge from a CI run. State that exception instead
  of the old never-contradicts claim; name codelore check authoritative.

- CHANGELOG [Unreleased]: add the check --cache-dir flag line.
@emrecdr emrecdr merged commit 28ccf14 into main Jul 10, 2026
7 of 9 checks passed
@emrecdr emrecdr deleted the feat/sarif-fusion branch July 10, 2026 06:47
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.

2 participants