Align D&D validator ordering

This commit is contained in:
2026-07-22 14:30:20 +00:00
parent 748e02db80
commit 7b2fb0880d
10 changed files with 115 additions and 19 deletions

View File

@@ -32,10 +32,10 @@ func registerDefaultChains(registry *pipeline.ValidatorChainRegistry) error {
Module: spellextract.Key,
Validators: []pipeline.ModuleBinding{
pipeline.Binding(validjson.Key),
pipeline.Binding(validjsonschema.Key),
pipeline.Binding(spellshape.Key),
pipeline.Binding(spellcatalog.Key),
pipeline.Binding(spellsourcerefs.Key),
pipeline.Binding(validjsonschema.Key),
pipeline.Binding(spellrelatedness.Key),
},
})
@@ -46,10 +46,10 @@ func registerDefaultChains(registry *pipeline.ValidatorChainRegistry) error {
Module: spellnormalize.Key,
Validators: []pipeline.ModuleBinding{
pipeline.Binding(validjson.Key),
pipeline.Binding(validjsonschema.Key),
pipeline.Binding(spellshape.Key),
pipeline.Binding(spellcatalog.Key),
pipeline.Binding(spellsourcerefs.Key),
pipeline.Binding(validjsonschema.Key),
pipeline.Binding(spellrelatedness.Key),
},
})
@@ -60,9 +60,9 @@ func registerDefaultChains(registry *pipeline.ValidatorChainRegistry) error {
Module: npcextract.Key,
Validators: []pipeline.ModuleBinding{
pipeline.Binding(validjson.Key),
pipeline.Binding(validjsonschema.Key),
pipeline.Binding(npcshape.Key),
pipeline.Binding(npcsourcerefs.Key),
pipeline.Binding(validjsonschema.Key),
pipeline.Binding(npcrelatedness.Key),
},
})
@@ -73,10 +73,10 @@ func registerDefaultChains(registry *pipeline.ValidatorChainRegistry) error {
Module: npcnormalize.Key,
Validators: []pipeline.ModuleBinding{
pipeline.Binding(validjson.Key),
pipeline.Binding(validjsonschema.Key),
pipeline.Binding(npcshape.Key),
pipeline.Binding(npcidentity.Key),
pipeline.Binding(npcsourcerefs.Key),
pipeline.Binding(validjsonschema.Key),
pipeline.Binding(npcrelatedness.Key),
},
})

View File

@@ -52,10 +52,10 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
})
wantChain := []pipeline.ModuleBinding{
pipeline.Binding("generic/valid_json"),
pipeline.Binding("generic/valid_json_schema"),
pipeline.Binding("extract/dnd/spells/shape"),
pipeline.Binding("extract/dnd/spells/catalog"),
pipeline.Binding("extract/dnd/spells/source_refs"),
pipeline.Binding("generic/valid_json_schema"),
pipeline.Binding("extract/dnd/spells/source_relatedness"),
}
if got := registries.ValidatorChains.Validators(pipeline.StageExtract, spells.Key); !reflect.DeepEqual(got, wantChain) {
@@ -66,9 +66,9 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
}
npcExtractChain := []pipeline.ModuleBinding{
pipeline.Binding("generic/valid_json"),
pipeline.Binding("generic/valid_json_schema"),
pipeline.Binding("extract/dnd/npcs/shape"),
pipeline.Binding("extract/dnd/npcs/source_refs"),
pipeline.Binding("generic/valid_json_schema"),
pipeline.Binding("extract/dnd/npcs/source_relatedness"),
}
if got := registries.ValidatorChains.Validators(pipeline.StageExtract, npcextract.Key); !reflect.DeepEqual(got, npcExtractChain) {
@@ -76,10 +76,10 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
}
npcNormalizeChain := []pipeline.ModuleBinding{
pipeline.Binding("generic/valid_json"),
pipeline.Binding("generic/valid_json_schema"),
pipeline.Binding("extract/dnd/npcs/shape"),
pipeline.Binding("normalize/dnd/npcs/identity"),
pipeline.Binding("extract/dnd/npcs/source_refs"),
pipeline.Binding("generic/valid_json_schema"),
pipeline.Binding("extract/dnd/npcs/source_relatedness"),
}
if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, npcnormalize.Key); !reflect.DeepEqual(got, npcNormalizeChain) {

View File

@@ -17,6 +17,7 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
dndregister "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/register"
npcshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcs/shape"
genericregister "gitea.maximumdirect.net/eric/notarius/internal/modules/generic/register"
"gitea.maximumdirect.net/eric/notarius/internal/modules/seriatim/input/transcript"
seriatimregister "gitea.maximumdirect.net/eric/notarius/internal/modules/seriatim/register"
@@ -130,6 +131,35 @@ func TestRunnerProcessesSeriatimInputWithProductionDNDNPCPipeline(t *testing.T)
}
}
func TestProductionNPCPipelineAttributesInvalidShapeBeforeSchemaValidation(t *testing.T) {
registries := productionNPCRegistries(t)
effective, err := loadNPCPipelineConfig(t).Resolve(config.ResolveInput{
PipelineID: "dnd-npcs-fixture",
Catalog: moduleCatalog(registries),
})
if err != nil {
t.Fatalf("Resolve() error = %v, want nil", err)
}
client := &fakeNPCProductionLLMClient{response: npcProductionResponse{NPCs: []npcProductionRecord{{
Name: "",
Aliases: []string{},
Description: "A participant.",
Relationships: []npcProductionRelationship{},
SourceRefs: []npcProductionSourceRef{{StartUnitID: 1, EndUnitID: 1}},
}}}}
output, err := runPreparedPipeline(t, registries, effective.ResolvedPipeline, client, pipeline.RunInput{RawInput: readNPCFixture(t)})
if err != nil {
t.Fatalf("Run() error = %v, want non-fatal rejected output", err)
}
if len(client.requests) != 3 || len(output.Rejected) != 1 || len(output.NormalizeOutputs) != 0 {
t.Fatalf("LLM requests = %d rejected = %#v normalized = %#v, want exhausted shape rejection", len(client.requests), output.Rejected, output.NormalizeOutputs)
}
rejection := output.Rejected[0]
if rejection.ReasonCode != npcshape.ReasonCode || rejection.ValidatorName != npcshape.Key || rejection.AttemptCount != 3 {
t.Fatalf("rejection = %#v, want exhausted NPC shape rejection", rejection)
}
}
type npcProductionResponse struct {
NPCs []npcProductionRecord `json:"npcs"`
}

View File

@@ -22,13 +22,22 @@ type spellCastResponse struct {
}
type fakeSpellsLLMClient struct {
response extractionResponse
requests []contracts.StructuredCompletionRequest
response extractionResponse
responses []extractionResponse
requests []contracts.StructuredCompletionRequest
}
func (client *fakeSpellsLLMClient) CompleteStructured(_ context.Context, req contracts.StructuredCompletionRequest, out any) (contracts.StructuredCompletionResponse, error) {
client.requests = append(client.requests, cloneStructuredCompletionRequest(req))
content, err := json.Marshal(client.response)
response := client.response
if client.responses != nil {
index := len(client.requests) - 1
if index >= len(client.responses) {
return contracts.StructuredCompletionResponse{}, fmt.Errorf("missing fake response %d", index)
}
response = client.responses[index]
}
content, err := json.Marshal(response)
if err != nil {
return contracts.StructuredCompletionResponse{}, err
}

View File

@@ -12,6 +12,8 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
spellnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/spells"
spellshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/spells/shape"
"gitea.maximumdirect.net/eric/notarius/internal/modules/seriatim/input/transcript"
)
@@ -164,6 +166,47 @@ func TestRunnerPassesPartyAndGlossaryReferencesToDNDSpellsPrompt(t *testing.T) {
}
}
func TestProductionSpellPipelineAttributesInvalidShapeBeforeSchemaValidation(t *testing.T) {
registries := productionNPCRegistries(t)
configValue := config.Default()
configValue.Pipelines["dnd-spells-shape"] = pipeline.PipelineProfile{
Input: pipeline.Binding(transcript.Key),
Chunk: pipeline.ModuleBinding{Module: pipeline.DefaultChunkModule, Options: map[string]any{"max_units": 100}},
Artifacts: map[string]pipeline.ArtifactLaneProfile{
"spells": {
Extract: pipeline.ModuleBinding{Module: spells.Key, Retries: 2},
Normalize: pipeline.Binding(spellnormalize.Key),
},
},
}
effective, err := configValue.Resolve(config.ResolveInput{
PipelineID: "dnd-spells-shape",
Catalog: moduleCatalog(registries),
})
if err != nil {
t.Fatalf("Resolve() error = %v, want nil", err)
}
response := extractionResponse{SpellCasts: []spellCastResponse{{
Caster: "Aria",
Spell: "Cure Wounds",
Effect: "",
NarrativeDescription: "Aria casts the spell.",
SourceRefs: responseSourceRefs("spell-session", 1, 1),
}}}
client := &fakeSpellsLLMClient{responses: []extractionResponse{response, response, response}}
output, err := runPreparedPipeline(t, registries, effective.ResolvedPipeline, client, pipeline.RunInput{RawInput: readDNDSpellsFixture(t)})
if err != nil {
t.Fatalf("Run() error = %v, want non-fatal rejected output", err)
}
if len(client.requests) != 3 || len(output.Rejected) != 1 || len(output.NormalizeOutputs) != 0 {
t.Fatalf("LLM requests = %d rejected = %#v normalized = %#v, want exhausted shape rejection", len(client.requests), output.Rejected, output.NormalizeOutputs)
}
rejection := output.Rejected[0]
if rejection.ReasonCode != spellshape.ReasonCode || rejection.ValidatorName != spellshape.Key || rejection.AttemptCount != 3 {
t.Fatalf("rejection = %#v, want exhausted spell shape rejection", rejection)
}
}
func dndSpellsReferenceSet(party string, glossary string) contracts.ReferenceSet {
slots := make(map[string]contracts.ResolvedReferenceSlot)
if strings.TrimSpace(party) != "" {