Document combat semantics validator evaluation

This commit is contained in:
2026-08-28 00:23:50 +00:00
parent 9cb7462800
commit 22e6caa2a0
5 changed files with 92 additions and 1 deletions

View File

@@ -0,0 +1,10 @@
[
{"name":"active encounter","transcript_units":[{"id":1,"text":"Roll initiative; the goblins attack."},{"id":2,"text":"The ranger hits and deals damage."}],"proposed_kind":"combat","expected_verdict":"approved","reviewer_rationale":"Initiative and hostile actions organize the chunk."},
{"name":"combat after setup","transcript_units":[{"id":1,"text":"They open the crypt door."},{"id":2,"text":"Skeletons attack and turns begin."}],"proposed_kind":"narrative","expected_verdict":"combat_should_be_added","reviewer_rationale":"Brief setup does not displace substantive active combat."},
{"name":"combat aftermath","transcript_units":[{"id":1,"text":"The last enemy falls."},{"id":2,"text":"They search bodies and heal."}],"proposed_kind":"combat","expected_verdict":"combat_should_be_removed","reviewer_rationale":"Looting and healing after a completed fight are not active combat."},
{"name":"multi phase encounter","transcript_units":[{"id":1,"text":"The dragon attacks."},{"id":2,"text":"After a rules clarification, its next turn begins."}],"proposed_kind":"combat","expected_verdict":"approved","reviewer_rationale":"A brief rules interruption does not end the encounter."},
{"name":"planning","transcript_units":[{"id":1,"text":"They plan how to ambush the guard."}],"proposed_kind":"combat","expected_verdict":"combat_should_be_removed","reviewer_rationale":"Planning a possible fight is not active encounter play."},
{"name":"hostile dialogue","transcript_units":[{"id":1,"text":"The captain threatens them and they argue."}],"proposed_kind":"combat","expected_verdict":"combat_should_be_removed","reviewer_rationale":"Threats and hostile dialogue alone are insufficient."},
{"name":"recap recollection","transcript_units":[{"id":1,"text":"They recap last session's battle with the lich."}],"proposed_kind":"combat","expected_verdict":"combat_should_be_removed","reviewer_rationale":"Recounting earlier combat is not current active combat."},
{"name":"rules discussion","transcript_units":[{"id":1,"text":"The table discusses how concentration works."}],"proposed_kind":"meta","expected_verdict":"approved","reviewer_rationale":"Sustained out-of-character rules discussion has no active encounter."}
]

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"os"
"reflect"
"strings"
"testing"
@@ -167,6 +168,54 @@ func TestValidatorConstructionOptionsAndMetadata(t *testing.T) {
}
}
func TestEvaluationCasesAreStrictAndCoverVerdicts(t *testing.T) {
content, err := os.ReadFile("testdata/evaluation_cases.json")
if err != nil {
t.Fatal(err)
}
var cases []struct {
Name string `json:"name"`
TranscriptUnits []struct {
ID int `json:"id"`
Text string `json:"text"`
} `json:"transcript_units"`
ProposedKind string `json:"proposed_kind"`
ExpectedVerdict string `json:"expected_verdict"`
ReviewerRationale string `json:"reviewer_rationale"`
}
decoder := json.NewDecoder(strings.NewReader(string(content)))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&cases); err != nil {
t.Fatalf("decode evaluation cases: %v", err)
}
seenNames, seenVerdicts := map[string]bool{}, map[string]bool{}
for _, value := range cases {
if strings.TrimSpace(value.Name) == "" || seenNames[value.Name] || len(value.TranscriptUnits) == 0 || strings.TrimSpace(value.ProposedKind) == "" || strings.TrimSpace(value.ReviewerRationale) == "" {
t.Fatalf("invalid evaluation case: %#v", value)
}
seenNames[value.Name] = true
switch value.ProposedKind {
case "combat", "narrative", "recap", "meta":
default:
t.Fatalf("unsupported proposed kind %q", value.ProposedKind)
}
switch value.ExpectedVerdict {
case "approved", "combat_should_be_added", "combat_should_be_removed":
seenVerdicts[value.ExpectedVerdict] = true
default:
t.Fatalf("unsupported verdict %q", value.ExpectedVerdict)
}
for _, unit := range value.TranscriptUnits {
if unit.ID <= 0 || strings.TrimSpace(unit.Text) == "" {
t.Fatalf("invalid transcript unit: %#v", unit)
}
}
}
if len(seenVerdicts) != 3 {
t.Fatalf("evaluation verdicts = %#v, want all classes", seenVerdicts)
}
}
func newValidator(t *testing.T, client contracts.StructuredLLMClient) *Validator {
t.Helper()
validator, err := New(client, Options{})