Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

146 lines
5.0 KiB
Go

package review
import (
"time"
"github.com/zzet/gortex/internal/persistence"
)
// SuppressionStore is the durable false-positive filter for review findings. It
// wraps the SQLite sidecar's suppressions table: a finding whose IdentityKey is
// recorded for a repo is silently dropped from every subsequent review of that
// repo, permanently, until it is explicitly un-suppressed.
//
// This is distinct from a development memory or a feedback signal: it is not
// ranked, not surfaced, not decayed. It is a hard, finding-identity-keyed
// never-flag-again list, scoped per repo (the same repoKey the notes / memories
// managers use), and it survives daemon restarts because it lives in the sidecar.
//
// A nil *SuppressionStore — and a store wrapping a nil sidecar — is tolerated by
// every method: IsSuppressed returns false, the mutators are no-ops, and List
// returns nothing. So a review flow can hold a nil store with no guard.
type SuppressionStore struct {
sidecar *persistence.SidecarStore
}
// NewSuppressionStore binds a suppression store to an already-open sidecar. A
// nil sidecar yields a store that tolerates every call as a no-op / false.
func NewSuppressionStore(sidecar *persistence.SidecarStore) *SuppressionStore {
return &SuppressionStore{sidecar: sidecar}
}
// SuppressionRow is the read-side projection of a stored suppression — the
// denormalised finding context plus the hit bookkeeping.
type SuppressionRow struct {
IdentityKey string `json:"identity_key"`
Rule string `json:"rule"`
Category string `json:"category"`
File string `json:"file"`
SymbolID string `json:"symbol_id"`
Reason string `json:"reason,omitempty"`
Author string `json:"author,omitempty"`
HitCount int64 `json:"hit_count"`
Created time.Time `json:"created_at"`
LastHit time.Time `json:"last_hit,omitempty"`
}
// usable reports whether the store has a live sidecar to read / write.
func (s *SuppressionStore) usable() bool {
return s != nil && s.sidecar != nil
}
// Suppress records (or refreshes) a suppression for a finding in a repo. The
// IdentityKey is computed from the finding if it does not already carry one, so
// a caller can pass a bare finding. An existing row's hit bookkeeping is
// preserved (the finding's own counters are not trusted here); only the context
// and the suppress reason / author are refreshed. A nil store is a no-op.
func (s *SuppressionStore) Suppress(repoKey string, f Finding, reason, author string) error {
if !s.usable() {
return nil
}
key := f.IdentityKey
if key == "" {
key = IdentityKey(f)
}
now := time.Now().UTC()
created := now
var hits int64
var lastHit time.Time
if prior, ok := s.sidecar.LoadSuppression(repoKey, key); ok {
// Preserve the original creation time and accumulated hit counters so a
// re-suppress (e.g. with a corrected reason) does not reset history.
if !prior.Created.IsZero() {
created = prior.Created
}
hits = prior.HitCount
lastHit = prior.LastHit
}
return s.sidecar.UpsertSuppression(repoKey, persistence.SuppressionEntry{
IdentityKey: key,
Rule: f.Rule,
Category: f.Category,
File: f.File,
SymbolID: f.SymbolID,
Reason: reason,
Author: author,
HitCount: hits,
Created: created,
LastHit: lastHit,
})
}
// IsSuppressed is the hot read inside the gate: it reports whether a finding
// identity is suppressed for a repo and, on a hit, bumps the row's hit_count /
// last_hit so the suppression's usefulness is observable. A nil store / sidecar,
// or an empty key, returns false. A failed bump never changes the answer — the
// finding is still suppressed.
func (s *SuppressionStore) IsSuppressed(repoKey, identityKey string) bool {
if !s.usable() || identityKey == "" {
return false
}
if _, ok := s.sidecar.LoadSuppression(repoKey, identityKey); !ok {
return false
}
_ = s.sidecar.BumpSuppressionHit(repoKey, identityKey, time.Now().UTC())
return true
}
// List returns every suppression for a repo, most-recently-hit first. A nil
// store / sidecar returns an empty slice and no error.
func (s *SuppressionStore) List(repoKey string) ([]SuppressionRow, error) {
if !s.usable() {
return nil, nil
}
entries, err := s.sidecar.LoadSuppressions(repoKey)
if err != nil {
return nil, err
}
out := make([]SuppressionRow, 0, len(entries))
for _, e := range entries {
out = append(out, SuppressionRow{
IdentityKey: e.IdentityKey,
Rule: e.Rule,
Category: e.Category,
File: e.File,
SymbolID: e.SymbolID,
Reason: e.Reason,
Author: e.Author,
HitCount: e.HitCount,
Created: e.Created,
LastHit: e.LastHit,
})
}
return out, nil
}
// Unsuppress removes a suppression so the finding can be flagged again. A nil
// store / sidecar, or a missing row, is a no-op (not an error).
func (s *SuppressionStore) Unsuppress(repoKey, identityKey string) error {
if !s.usable() || identityKey == "" {
return nil
}
return s.sidecar.DeleteSuppression(repoKey, identityKey)
}