Improve scene-aware combat gating

This commit is contained in:
2026-07-25 21:08:24 +00:00
parent 3da20e9d6a
commit 2a7e025251
5 changed files with 107 additions and 14 deletions

View File

@@ -56,12 +56,13 @@ type Registry struct {
}
// Resolver holds an immutable construction-time view and memoizes immutable
// operation-time views by eligibility digest.
// operation-time views by raw reference identity and eligibility digest.
type Resolver struct {
seeded *Registry
mu sync.Mutex
cache map[string]*Registry
mu sync.Mutex
cache map[string]*Registry
rawCache map[string]*Registry
}
// NewResolver validates a materialized construction-time scene reference. An
@@ -72,7 +73,11 @@ func NewResolver(references contracts.ReferenceSet) (*Resolver, error) {
if err != nil {
return nil, err
}
return &Resolver{seeded: seeded, cache: make(map[string]*Registry)}, nil
return &Resolver{
seeded: seeded,
cache: make(map[string]*Registry),
rawCache: make(map[string]*Registry),
}, nil
}
func constructionReferences(references contracts.ReferenceSet) contracts.ReferenceSet {
@@ -106,23 +111,59 @@ func (r *Resolver) Resolve(references contracts.ReferenceSet) (*Registry, error)
return r.seeded, nil
}
slot := references.Slots[ReferenceSlot]
rawKey := ""
if len(slot.Items) == 1 {
rawKey = rawReferenceKey(slot.Items[0])
}
r.mu.Lock()
defer r.mu.Unlock()
if rawKey != "" {
if cached, ok := r.rawCache[rawKey]; ok {
return cached, nil
}
}
resolved, err := Resolve(references)
if err != nil {
return nil, err
}
r.mu.Lock()
defer r.mu.Unlock()
if sameEligibility(r.seeded, resolved) {
if rawKey != "" {
r.rawCache[rawKey] = r.seeded
}
return r.seeded, nil
}
if cached, ok := r.cache[resolved.EligibilityDigest()]; ok {
if rawKey != "" {
r.rawCache[rawKey] = cached
}
return cached, nil
}
r.cache[resolved.EligibilityDigest()] = resolved
if rawKey != "" {
r.rawCache[rawKey] = resolved
}
return resolved, nil
}
func rawReferenceKey(item contracts.ReferenceItem) string {
digest := strings.ToLower(strings.TrimSpace(item.Digest))
if !validSHA256Digest(digest) {
digest = semanticDigest(item.Content)
}
return strings.ToLower(strings.TrimSpace(item.MediaType)) + "\x00" + digest
}
func validSHA256Digest(value string) bool {
if len(value) != len("sha256:")+sha256.Size*2 || !strings.HasPrefix(value, "sha256:") {
return false
}
decoded, err := hex.DecodeString(strings.TrimPrefix(value, "sha256:"))
return err == nil && len(decoded) == sha256.Size
}
func sameEligibility(first, second *Registry) bool {
if first == nil || second == nil {
return first == second

View File

@@ -129,6 +129,55 @@ func TestResolverUsesGeneratedReferenceWhenPresent(t *testing.T) {
}
}
func TestResolverReusesRawAndSemanticallyEquivalentViews(t *testing.T) {
baseContent := encodeList(t, sceneList(
scene("chunk-1", "session-alpha", 1, 2, dnd.SceneKindCombat, "Original title", "Original summary."),
))
baseItem := referenceItem(baseContent)
baseItem.Digest = semanticDigest(baseContent)
resolver, err := NewResolver(contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{ReferenceSlot: {}}})
if err != nil {
t.Fatal(err)
}
first, err := resolver.Resolve(referenceSet(baseItem))
if err != nil {
t.Fatal(err)
}
second, err := resolver.Resolve(referenceSet(baseItem))
if err != nil {
t.Fatal(err)
}
if second != first {
t.Fatal("identical framework reference did not reuse its immutable view")
}
equivalentContent := encodeList(t, sceneList(
scene("chunk-1", "session-alpha", 1, 2, dnd.SceneKindCombat, "Revised title", "Revised summary."),
))
equivalentItem := referenceItem(equivalentContent)
equivalentItem.Digest = semanticDigest(equivalentContent)
equivalent, err := resolver.Resolve(referenceSet(equivalentItem))
if err != nil {
t.Fatal(err)
}
if equivalent != first {
t.Fatal("prose-only change did not reuse the semantic eligibility view")
}
for _, digest := range []string{"", "not-a-sha256-digest"} {
fallbackItem := referenceItem(baseContent)
fallbackItem.Digest = digest
fallback, resolveErr := resolver.Resolve(referenceSet(fallbackItem))
if resolveErr != nil {
t.Fatal(resolveErr)
}
if fallback != first {
t.Fatalf("digest fallback %q did not reuse the content-addressed view", digest)
}
}
}
func TestRegistryIsImmutableAndResolverIsSafeForConcurrentReuse(t *testing.T) {
content := encodeList(t, sceneList(scene("chunk-1", "session-alpha", 1, 2, dnd.SceneKindCombat, "Title", "Summary.")))
references := referenceSet(referenceItem(content))