Update D&D schemas to require integer unit_id values

This commit is contained in:
2026-07-06 14:41:24 -05:00
parent 79a585d17e
commit aec807fcb0
18 changed files with 403 additions and 79 deletions

View File

@@ -1,4 +1,5 @@
Source references must use the source-unit IDs exactly as provided.
Source references must use 1-based integer source-unit numbers from the
transcript, where 1 is the first provided source unit.
Return only D&D spell-cast artifacts. For each spell cast, identify the in-world
caster, spell name, effect, narrative description, and source references using

View File

@@ -47,12 +47,12 @@
"minLength": 1
},
"start_unit_id": {
"type": "string",
"minLength": 1
"type": "integer",
"minimum": 1
},
"end_unit_id": {
"type": "string",
"minLength": 1
"type": "integer",
"minimum": 1
}
}
}

View File

@@ -133,7 +133,7 @@ func (e *Extractor) Extract(ctx context.Context, req contracts.ExtractionRequest
}
candidates = append(candidates, artifacts.ArtifactCandidate{
Payload: payload,
SourceRefs: append([]source.SourceRef(nil), spellCast.SourceRefs...),
SourceRefs: sourceRefCandidates(req.Source, spellCast.SourceRefs),
})
}
return contracts.ExtractionResult{Candidates: candidates}, nil
@@ -164,6 +164,17 @@ func spellCastPayload(spellCast spellCastResponse) (json.RawMessage, error) {
})
}
func sourceRefCandidates(doc *source.SourceDocument, refs []dnd.SourceRefResponse) []source.SourceRef {
if len(refs) == 0 {
return nil
}
out := make([]source.SourceRef, 0, len(refs))
for _, ref := range refs {
out = append(out, dnd.SourceRefCandidate(doc, ref))
}
return out
}
func extractorErrorf(format string, args ...any) error {
return fmt.Errorf("dnd spells extractor: "+format, args...)
}

View File

@@ -21,9 +21,7 @@ func TestExtractReturnsSpellCandidateFromStructuredOutput(t *testing.T) {
Spell: " Cure Wounds ",
Effect: " Heals an injured ally. ",
NarrativeDescription: " Aria restores the fighter after the fight. ",
SourceRefs: []source.SourceRef{
{SourceID: "session-alpha", StartUnitID: "seg-001", EndUnitID: "seg-002"},
},
SourceRefs: responseSourceRefsInt("session-alpha", 1, 2),
},
},
},
@@ -256,14 +254,14 @@ func TestExtractPreservesResponseOrder(t *testing.T) {
Spell: "Cure Wounds",
Effect: "Heals.",
NarrativeDescription: "First spell.",
SourceRefs: []source.SourceRef{{SourceID: "session-alpha", StartUnitID: "seg-001", EndUnitID: "seg-001"}},
SourceRefs: responseSourceRefs("session-alpha", "seg-001", "seg-001"),
},
{
Caster: "Bandit Shaman",
Spell: "Fire Bolt",
Effect: "Burns.",
NarrativeDescription: "Second spell.",
SourceRefs: []source.SourceRef{{SourceID: "session-alpha", StartUnitID: "seg-002", EndUnitID: "seg-002"}},
SourceRefs: responseSourceRefs("session-alpha", "seg-002", "seg-002"),
},
},
},
@@ -298,7 +296,7 @@ func TestExtractCopiesCandidateSourceRefs(t *testing.T) {
Spell: "Cure Wounds",
Effect: "Heals.",
NarrativeDescription: "Aria heals.",
SourceRefs: []source.SourceRef{{SourceID: "session-alpha", StartUnitID: "seg-001", EndUnitID: "seg-002"}},
SourceRefs: responseSourceRefs("session-alpha", "seg-001", "seg-002"),
},
},
},
@@ -308,7 +306,7 @@ func TestExtractCopiesCandidateSourceRefs(t *testing.T) {
if err != nil {
t.Fatalf("Extract() error = %v, want nil", err)
}
client.response.SpellCasts[0].SourceRefs[0].StartUnitID = "mutated"
client.response.SpellCasts[0].SourceRefs[0].StartUnitID = dnd.UnitRefFromString("mutated")
if got := result.Candidates[0].SourceRefs[0].StartUnitID; got != "seg-001" {
t.Fatalf("candidate source ref start = %q, want copied seg-001", got)

View File

@@ -1,6 +1,6 @@
package spells
import "gitea.maximumdirect.net/eric/notarius/internal/core/source"
import "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd"
type SpellCast struct {
Caster string `json:"caster"`
@@ -14,9 +14,9 @@ type extractionResponse struct {
}
type spellCastResponse struct {
Caster string `json:"caster"`
Spell string `json:"spell"`
Effect string `json:"effect"`
NarrativeDescription string `json:"narrative_description"`
SourceRefs []source.SourceRef `json:"source_refs"`
Caster string `json:"caster"`
Spell string `json:"spell"`
Effect string `json:"effect"`
NarrativeDescription string `json:"narrative_description"`
SourceRefs []dnd.SourceRefResponse `json:"source_refs"`
}

View File

@@ -26,18 +26,14 @@ func TestRunnerProcessesSeriatimInputWithDNDSpellsExtractor(t *testing.T) {
Spell: "Cure Wounds",
Effect: "Heals an injured ally.",
NarrativeDescription: "Aria restores the fighter after the fight.",
SourceRefs: []source.SourceRef{
{SourceID: expectedDoc.ID, StartUnitID: "seg-001", EndUnitID: "seg-001"},
},
SourceRefs: responseSourceRefs(expectedDoc.ID, "seg-001", "seg-001"),
},
{
Caster: "Borin",
Spell: "Fire Bolt",
Effect: "Scorches the wight.",
NarrativeDescription: "Borin hurls fire at the wight.",
SourceRefs: []source.SourceRef{
{SourceID: expectedDoc.ID, StartUnitID: "seg-003", EndUnitID: "seg-003"},
},
SourceRefs: responseSourceRefs(expectedDoc.ID, "seg-003", "seg-003"),
},
},
},
@@ -126,9 +122,7 @@ func TestRunnerPassesPartyAndGlossaryReferencesToDNDSpellsPrompt(t *testing.T) {
Spell: "Fire Bolt",
Effect: "Scorches the wight.",
NarrativeDescription: "Borin hurls fire at the wight.",
SourceRefs: []source.SourceRef{
{SourceID: expectedDoc.ID, StartUnitID: "seg-003", EndUnitID: "seg-003"},
},
SourceRefs: responseSourceRefs(expectedDoc.ID, "seg-003", "seg-003"),
},
},
},
@@ -213,9 +207,7 @@ func TestRunnerRejectsDNDSpellCastWithInvalidSourceRef(t *testing.T) {
Spell: "Cure Wounds",
Effect: "Heals an injured ally.",
NarrativeDescription: "Aria restores the fighter after the fight.",
SourceRefs: []source.SourceRef{
{SourceID: "spell-session", StartUnitID: "seg-999", EndUnitID: "seg-999"},
},
SourceRefs: responseSourceRefs("spell-session", "seg-999", "seg-999"),
},
},
},

View File

@@ -29,6 +29,23 @@ func TestLoadResponseSchemaForSpells(t *testing.T) {
if !json.Valid(schema.JSONSchema) {
t.Fatalf("schema.JSONSchema is invalid JSON: %s", schema.JSONSchema)
}
var decoded map[string]any
if err := json.Unmarshal(schema.JSONSchema, &decoded); err != nil {
t.Fatalf("Unmarshal(schema.JSONSchema) error = %v, want nil", err)
}
properties := decoded["properties"].(map[string]any)
spellCastProperties := properties["spell_casts"].(map[string]any)["items"].(map[string]any)["properties"].(map[string]any)
sourceRefProperties := spellCastProperties["source_refs"].(map[string]any)["items"].(map[string]any)["properties"].(map[string]any)
for _, field := range []string{"start_unit_id", "end_unit_id"} {
property := sourceRefProperties[field].(map[string]any)
if property["type"] != "integer" {
t.Fatalf("%s type = %#v, want integer", field, property["type"])
}
if property["minimum"] != float64(1) {
t.Fatalf("%s minimum = %#v, want 1", field, property["minimum"])
}
}
}
func TestResponseSchemaJSONIsMutationSafe(t *testing.T) {

View File

@@ -6,6 +6,7 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd"
)
func promptExtractionRequest() contracts.ExtractionRequest {
@@ -59,3 +60,23 @@ func mustJSON(t *testing.T, value any) string {
}
return string(encoded)
}
func responseSourceRefs(sourceID string, startUnitID string, endUnitID string) []dnd.SourceRefResponse {
return []dnd.SourceRefResponse{
{
SourceID: sourceID,
StartUnitID: dnd.UnitRefFromString(startUnitID),
EndUnitID: dnd.UnitRefFromString(endUnitID),
},
}
}
func responseSourceRefsInt(sourceID string, startUnitID int, endUnitID int) []dnd.SourceRefResponse {
return []dnd.SourceRefResponse{
{
SourceID: sourceID,
StartUnitID: dnd.UnitRefFromInt(startUnitID),
EndUnitID: dnd.UnitRefFromInt(endUnitID),
},
}
}