package loop import ( "crypto/sha256" "encoding/binary" "encoding/hex" "math" "github.com/kami/maven/internal/store" ) // DigestIdentity identifies one semantic occurrence of a rule while it is // waiting behind a restraint gate. It is deliberately produced by the rule, // beside its predicate: generated prose is presentation, not identity, and // State.Now advancing does not turn the same unmet condition into a new event. // // A nil or empty identity means the rule has not declared a safe durable // identity and therefore cannot enter the suppressed-nudge digest. Failing // closed here is cheaper and safer than inventing a generic state hash that // either changes every tick or silently ignores an input the rule actually // uses. type DigestIdentity func(State) []byte // DigestCandidateFingerprint returns the durable, opaque key used to decide // whether a suppressed candidate is already pending. Rule name and severity // are framed alongside the rule-owned identity so two rules can never alias // merely because they happen to read the same fact. func DigestCandidateFingerprint(r Rule, s State) (string, bool) { if r.DigestIdentity == nil { return "", false } identity := r.DigestIdentity(s) if len(identity) == 0 { return "", false } h := sha256.New() writeDigestFrame(h, []byte("maven-digest-candidate-v1")) writeDigestFrame(h, []byte(r.Name)) var severity [8]byte binary.BigEndian.PutUint64(severity[:], uint64(r.Severity)) writeDigestFrame(h, severity[:]) writeDigestFrame(h, identity) return hex.EncodeToString(h.Sum(nil)), true } type digestWriter interface { Write([]byte) (int, error) } func writeDigestFrame(w digestWriter, value []byte) { var size [8]byte binary.BigEndian.PutUint64(size[:], uint64(len(value))) _, _ = w.Write(size[:]) _, _ = w.Write(value) } // factDigestIdentity encodes the complete durable identity of one fact row. // A new fact row means a new observation even when its human-readable value // happens to be the same; changing any stored claim field also changes the // identity in synthetic states used by tests and simulations where ID may be // zero. func factDigestIdentity(f store.Fact) []byte { var fixed [32]byte binary.BigEndian.PutUint64(fixed[0:8], uint64(f.ID)) binary.BigEndian.PutUint64(fixed[8:16], uint64(f.Ts.UnixNano())) binary.BigEndian.PutUint64(fixed[16:24], math.Float64bits(f.Confidence)) if f.VoidsID.Valid { binary.BigEndian.PutUint64(fixed[24:32], uint64(f.VoidsID.Int64)) } out := make([]byte, 0, len(fixed)+len(f.Key)+len(f.Value)+len(f.Source)+len(f.Kind)+40) out = append(out, fixed[:]...) out = appendDigestFrame(out, []byte(f.Kind)) out = appendDigestFrame(out, []byte(f.Key)) out = appendDigestFrame(out, []byte(f.Value)) out = appendDigestFrame(out, []byte(f.Source)) return out } func appendDigestFrame(dst, value []byte) []byte { var size [8]byte binary.BigEndian.PutUint64(size[:], uint64(len(value))) dst = append(dst, size[:]...) return append(dst, value...) }