Complete minimal D&D extraction cutover
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/core/config"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcs"
|
||||
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs"
|
||||
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcs"
|
||||
)
|
||||
@@ -47,6 +48,10 @@ func TestProductionNPCConfigurationResolvesTypedLane(t *testing.T) {
|
||||
if !ok || !reflect.DeepEqual(normalizeSpec.Requires, []string{"merged"}) || !reflect.DeepEqual(normalizeSpec.Provides, []string{"normalized"}) {
|
||||
t.Fatalf("NPC normalizer spec = %#v, want merged/normalized capabilities", normalizeSpec)
|
||||
}
|
||||
codecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.NPCListKind)
|
||||
if !ok || codecSpec.Kind != dnd.NPCListKind || codecSpec.Schema.ID != npccodec.SchemaID || codecSpec.Schema.Version != npccodec.SchemaVersion {
|
||||
t.Fatalf("NPC codec spec = %#v, want typed v1 durable schema", codecSpec)
|
||||
}
|
||||
|
||||
wantExtractChain := []pipeline.ModuleBinding{
|
||||
pipeline.Binding("generic/valid_json"),
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user