Complete minimal D&D extraction cutover

This commit is contained in:
2026-07-22 19:23:10 +00:00
parent 90481a0e4b
commit 4b0b166143
13 changed files with 85 additions and 44 deletions

View File

@@ -82,7 +82,6 @@ func TestCodecStrictlyRejectsMalformedOrUnknownJSON(t *testing.T) {
}{
{name: "unknown top-level", raw: `{"npcs":[],"unexpected":true}`, want: "unknown field"},
{name: "unknown nested", raw: `{"npcs":[{"id":"x","name":"Mira","source_refs":[{"source_id":"s","start_unit_id":1,"end_unit_id":1}],"unexpected":true}]}`, want: "unknown field"},
{name: "removed enrichment", raw: `{"npcs":[{"id":"x","name":"Mira","aliases":[],"source_refs":[{"source_id":"s","start_unit_id":1,"end_unit_id":1}]}]}`, want: "unknown field"},
{name: "trailing", raw: `{"npcs":[]} {}`, want: "multiple JSON values"},
{name: "missing array", raw: `{}`, want: "npcs must be present"},
{name: "invalid reference", raw: `{"npcs":[{"id":"npc:sha256:0000000000000000000000000000000000000000000000000000000000000000","name":"Mira","source_refs":[{"source_id":"s","start_unit_id":0,"end_unit_id":1}]}]}`, want: "start_unit_id"},

View File

@@ -1,10 +1,8 @@
For every NPC record, return only the observed display name and transcript
units that support that identity.
Do not return descriptions, aliases, relationships, biographies, statistics,
alignment, motivations, encounter summaries, or lore inferred from general
D&D knowledge. Do not invent a label for an anonymous creature, crowd, or
generic role.
Return no other details or lore inferred from general D&D knowledge. Do not
invent a label for an anonymous creature, crowd, or generic role.
Preserve observed display spelling. Return at least one narrow source range for
every record.

View File

@@ -98,10 +98,9 @@ func TestRegistryLookupAndAccessorsAreImmutable(t *testing.T) {
}
}
func TestResolveRejectsRichOrInvalidRegistryJSON(t *testing.T) {
rich := []byte(`{"npcs":[{"id":"npc:sha256:99a16589618a04f535a7d21fdcc71a0b1c05d22f752cd492065b1086d97bc3d7","name":"Mira Thorn","aliases":[],"source_refs":[{"source_id":"session-alpha","start_unit_id":1,"end_unit_id":1}]}]}`)
func TestResolveRejectsMalformedOrUnsupportedRegistryInput(t *testing.T) {
for _, item := range []contracts.ReferenceItem{
{MediaType: "application/json", Content: rich},
{MediaType: "application/json", Content: []byte(`{"npcs":[`)},
{MediaType: "text/plain", Content: []byte(`{"npcs":[]}`)},
} {
_, err := Resolve(referenceSet(item))

View File

@@ -5,6 +5,7 @@ import (
"crypto/sha256"
"encoding/json"
"fmt"
"strings"
"sync"
"testing"
@@ -15,6 +16,7 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
combatcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/combatturns"
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcs"
spellcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/spells"
combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
@@ -34,7 +36,25 @@ func TestNPCOutputGroundsSpellAndCombatConsumersThroughOneOperation(t *testing.T
}
client := &groundedDNDLLMClient{}
output, err := runPreparedPipeline(t, registries, materialized, client, pipeline.RunInput{
prepared, err := pipeline.Prepare(materialized, registries, pipeline.ModuleDependencies{LLM: client})
if err != nil {
t.Fatalf("Prepare() error = %v", err)
}
for _, name := range []string{
"extract:npcs:dnd/npcs:prompt",
"extract:npcs:dnd/npcs:response_schema",
"extract:spells:dnd/spells:prompt",
"extract:spells:dnd/spells:response_schema",
"extract:spells:dnd/spells:npc_registry",
"extract:combat:dnd/combat-turns:prompt",
"extract:combat:dnd/combat-turns:response_schema",
"extract:combat:dnd/combat-turns:npc_registry",
"normalize:combat:dnd/combat-turns:npc_registry",
} {
assertCombatFingerprint(t, prepared.CheckpointFingerprints(), name)
}
output, err := pipeline.New().Run(context.Background(), pipeline.RunInput{
Prepared: prepared,
RawInput: readNPCFixture(t),
ExtractWorkers: 1,
})
@@ -44,6 +64,24 @@ func TestNPCOutputGroundsSpellAndCombatConsumersThroughOneOperation(t *testing.T
if len(output.Rejected) != 0 || len(output.NormalizeOutputs) != 3 {
t.Fatalf("run outputs = %#v rejected = %#v, want NPC, spell, and combat outputs", output.NormalizeOutputs, output.Rejected)
}
wantSchemas := map[string]string{"npcs": npccodec.SchemaID, "spells": spellcodec.SchemaID, "combat": combatcodec.SchemaID}
for _, serialized := range output.NormalizeOutputs {
if serialized.Artifact.Schema.ID != wantSchemas[serialized.LaneID] || serialized.Artifact.Schema.Version != "v1" {
t.Fatalf("%s artifact schema = %#v, want minimal v1 identity", serialized.LaneID, serialized.Artifact.Schema)
}
}
wantExtractorIdentity := map[string]struct{ promptID, schemaID string }{
"npcs": {npcs.PromptID, npcs.ResponseSchemaID},
"spells": {spells.PromptID, spells.ResponseSchemaID},
"combat": {combatextract.PromptID, combatextract.ResponseSchemaID},
}
for _, lane := range output.Manifest.ArtifactLanes {
want, ok := wantExtractorIdentity[lane.ID]
metadata, metadataOK := lane.Metadata["extractor"].(map[string]any)
if !ok || !metadataOK || metadata["prompt_id"] != want.promptID || metadata["prompt_version"] != "v1" || metadata["response_schema_id"] != want.schemaID || metadata["response_schema_version"] != "v1" {
t.Fatalf("%s extractor metadata = %#v, want v1 prompt/schema identity", lane.ID, metadata)
}
}
var npcPayload []byte
for _, serialized := range output.NormalizeOutputs {
@@ -94,6 +132,15 @@ func TestNPCOutputGroundsSpellAndCombatConsumersThroughOneOperation(t *testing.T
if provenanceCount != 3 {
t.Fatalf("NPC generated provenance count = %d, want spell extract plus combat extract/normalize", provenanceCount)
}
manifestContent, err := json.Marshal(output.Manifest)
if err != nil {
t.Fatal(err)
}
for _, forbidden := range []string{"Mira Thorn", "Hooded Guard", "For every NPC record", "Extract Dungeons & Dragons"} {
if strings.Contains(string(manifestContent), forbidden) {
t.Fatalf("manifest exposes prompt or reference payload content %q", forbidden)
}
}
for _, lane := range output.Manifest.ArtifactLanes {
for _, component := range []string{"extractor", "normalizer"} {
metadata, ok := lane.Metadata[component].(map[string]any)