feat(routing): canonical request hash
SHA-256 of (system, user) joined with 0x00 separator, truncated to uint64. Drives deterministic sample-band routing: identical prompt pair → same hash → same local-vs-Claude decision on every call. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
21
internal/routing/hash.go
Normal file
21
internal/routing/hash.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/binary"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CanonicalHash returns a deterministic 64-bit hash of (system, user).
|
||||||
|
// Used to make sample-band routing decisions reproducible: identical input
|
||||||
|
// strings produce the same hash on every call, independent of process state.
|
||||||
|
//
|
||||||
|
// Inputs are joined with a 0x00 byte separator before hashing — distinguishes
|
||||||
|
// (system="ab", user="cd") from (system="abcd", user="").
|
||||||
|
func CanonicalHash(system, user string) uint64 {
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write([]byte(system))
|
||||||
|
h.Write([]byte{0})
|
||||||
|
h.Write([]byte(user))
|
||||||
|
sum := h.Sum(nil)
|
||||||
|
return binary.BigEndian.Uint64(sum[:8])
|
||||||
|
}
|
||||||
46
internal/routing/hash_test.go
Normal file
46
internal/routing/hash_test.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package routing_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mathiasbq/supervisor/internal/routing"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCanonicalHashDeterministic(t *testing.T) {
|
||||||
|
a := routing.CanonicalHash("system one", "user one")
|
||||||
|
b := routing.CanonicalHash("system one", "user one")
|
||||||
|
assert.Equal(t, a, b, "same inputs must produce same hash")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanonicalHashDistinguishesInputs(t *testing.T) {
|
||||||
|
cases := [][2]string{
|
||||||
|
{"sys", "user"},
|
||||||
|
{"sys", "user2"},
|
||||||
|
{"sys2", "user"},
|
||||||
|
{"", "system\x00user"}, // separator collision attempt
|
||||||
|
{"system\x00user", ""},
|
||||||
|
}
|
||||||
|
seen := make(map[uint64]bool)
|
||||||
|
for _, c := range cases {
|
||||||
|
h := routing.CanonicalHash(c[0], c[1])
|
||||||
|
assert.False(t, seen[h], "collision on %v", c)
|
||||||
|
seen[h] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanonicalHashLowBitDistribution(t *testing.T) {
|
||||||
|
// Sanity check: across 1000 distinct inputs, low-bit split is roughly even.
|
||||||
|
zeros, ones := 0, 0
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
h := routing.CanonicalHash("sys", string(rune('a'+(i%26)))+string(rune(i)))
|
||||||
|
if h&1 == 0 {
|
||||||
|
zeros++
|
||||||
|
} else {
|
||||||
|
ones++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Allow ±15% deviation from 500/500. Tighter would be flaky on real data.
|
||||||
|
assert.InDelta(t, 500, zeros, 150)
|
||||||
|
assert.InDelta(t, 500, ones, 150)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user