Adopt registry-backed NPC occurrence artifacts

This commit is contained in:
2026-08-05 18:55:58 +00:00
parent 3f4a1f2647
commit 5e2ccffc0f
42 changed files with 676 additions and 528 deletions

View File

@@ -32,7 +32,9 @@ type Registry struct {
digest string
projectionDigest string
promptInput contracts.LLMInputMaterial
identityInput contracts.LLMInputMaterial
lookupByKey map[string]int
lookupByID map[string]int
}
// Resolver selects and memoizes immutable NPC registry views.
@@ -109,7 +111,9 @@ func emptyRegistry() *Registry {
canonical: append([]byte(nil), content...),
projectionDigest: projectionDigest,
promptInput: contracts.NewLLMInputMaterial(ReferenceSlot, npccodec.MediaType, content, projectionDigest, ""),
identityInput: contracts.NewLLMInputMaterial(ReferenceSlot, npccodec.MediaType, content, projectionDigest, ""),
lookupByKey: map[string]int{},
lookupByID: map[string]int{},
}
}
@@ -129,8 +133,10 @@ func loadRegistry(referenceContent []byte) (*Registry, error) {
list := cloneNPCRegistry(value)
lookupByKey := make(map[string]int, len(list.NPCs))
lookupByID := make(map[string]int, len(list.NPCs))
for index, npc := range list.NPCs {
lookupByKey[identity.ComparisonKey(npc.Name)] = index
lookupByID[npc.ID] = index
}
digest := semanticDigest(content)
projection, err := nameProjection(list)
@@ -138,6 +144,11 @@ func loadRegistry(referenceContent []byte) (*Registry, error) {
return nil, fmt.Errorf("encode NPC name projection: %w", err)
}
projectionDigest := semanticDigest(projection)
identityProjection, err := identityProjection(list)
if err != nil {
return nil, fmt.Errorf("encode NPC identity projection: %w", err)
}
identityProjectionDigest := semanticDigest(identityProjection)
return &Registry{
bound: true,
list: list,
@@ -146,6 +157,8 @@ func loadRegistry(referenceContent []byte) (*Registry, error) {
projectionDigest: projectionDigest,
promptInput: contracts.NewLLMInputMaterial(ReferenceSlot, npccodec.MediaType, projection, projectionDigest, ""),
lookupByKey: lookupByKey,
identityInput: contracts.NewLLMInputMaterial(ReferenceSlot, npccodec.MediaType, identityProjection, identityProjectionDigest, ""),
lookupByID: lookupByID,
}, nil
}
@@ -211,6 +224,15 @@ func (r *Registry) PromptInput() contracts.LLMInputMaterial {
return r.promptInput.Clone()
}
// IdentityPromptInput returns the ordered ID/name projection for consumers
// that must bind output records to exact registry identities.
func (r *Registry) IdentityPromptInput() contracts.LLMInputMaterial {
if r == nil {
return contracts.LLMInputMaterial{}
}
return r.identityInput.Clone()
}
// Lookup returns the canonical NPC for an exact canonical-name match under the
// NPC identity comparison policy.
func (r *Registry) Lookup(value string) (dnd.NPC, bool) {
@@ -224,6 +246,18 @@ func (r *Registry) Lookup(value string) (dnd.NPC, bool) {
return cloneNPC(r.list.NPCs[index]), true
}
// LookupID returns the canonical NPC for an exact durable ID.
func (r *Registry) LookupID(value string) (dnd.NPC, bool) {
if r == nil {
return dnd.NPC{}, false
}
index, ok := r.lookupByID[value]
if !ok {
return dnd.NPC{}, false
}
return cloneNPC(r.list.NPCs[index]), true
}
func semanticDigest(content []byte) string {
sum := sha256.Sum256(content)
return "sha256:" + hex.EncodeToString(sum[:])
@@ -233,10 +267,19 @@ type projectedNPC struct {
Name string `json:"name"`
}
type identityProjectedNPC struct {
ID string `json:"id"`
Name string `json:"name"`
}
type projectedNPCRegistry struct {
NPCs []projectedNPC `json:"npcs"`
}
type identityProjectedNPCRegistry struct {
NPCs []identityProjectedNPC `json:"npcs"`
}
func nameProjection(list dnd.NPCRegistry) ([]byte, error) {
projection := projectedNPCRegistry{NPCs: make([]projectedNPC, len(list.NPCs))}
for index, npc := range list.NPCs {
@@ -245,6 +288,14 @@ func nameProjection(list dnd.NPCRegistry) ([]byte, error) {
return json.Marshal(projection)
}
func identityProjection(list dnd.NPCRegistry) ([]byte, error) {
projection := identityProjectedNPCRegistry{NPCs: make([]identityProjectedNPC, len(list.NPCs))}
for index, npc := range list.NPCs {
projection.NPCs[index] = identityProjectedNPC{ID: npc.ID, Name: npc.Name}
}
return json.Marshal(projection)
}
func formatIdentityIssues(issues []identity.Issue) string {
parts := make([]string, len(issues))
for index, issue := range issues {

View File

@@ -19,7 +19,7 @@ func TestResolveUnboundRegistryHasExactEmptyProjection(t *testing.T) {
t.Fatalf("Resolve() error = %v", err)
}
input := registry.PromptInput()
if registry.Bound() || registry.Digest() != "" || registry.Count() != 0 || string(input.Content) != emptyPrompt {
if registry.Bound() || registry.Digest() != "" || registry.Count() != 0 || string(input.Content) != emptyPrompt || string(registry.IdentityPromptInput().Content) != emptyPrompt {
t.Fatalf("registry = %#v input = %#v, want unbound empty registry", registry, input)
}
if registry.ProjectionDigest() == "" || input.Digest != registry.ProjectionDigest() || input.OriginURI != "" {
@@ -46,6 +46,21 @@ func TestResolveKeepsDurableProvenanceAndProjectsOnlyOrderedNames(t *testing.T)
}
}
func TestIdentityPromptInputProjectsOrderedIDsAndNames(t *testing.T) {
registry := resolveList(t, registryFixture())
input := registry.IdentityPromptInput()
want := `{"npcs":[{"id":"` + identity.DeriveID("Mira Thorn") + `","name":"Mira Thorn"},{"id":"` + identity.DeriveID("Captain Vale") + `","name":"Captain Vale"}]}`
if string(input.Content) != want || input.Digest == registry.ProjectionDigest() {
t.Fatalf("identity projection = %#v, want %s", input, want)
}
if npc, ok := registry.LookupID(identity.DeriveID("Mira Thorn")); !ok || npc.Name != "Mira Thorn" {
t.Fatalf("LookupID() = %#v, %t", npc, ok)
}
if _, ok := registry.LookupID("npc:unknown"); ok {
t.Fatal("LookupID() accepted an unknown ID")
}
}
func TestNameProjectionDigestTracksOnlyNamesAndOrder(t *testing.T) {
base := registryFixture()
evidenceChanged := registryFixture()