Add D&D spells response schema

This commit is contained in:
2026-07-03 23:31:19 +00:00
parent d9b5c25cfe
commit 382ca3fb6c
4 changed files with 151 additions and 21 deletions

View File

@@ -0,0 +1,64 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "notarius.dnd.spells",
"type": "object",
"additionalProperties": false,
"required": ["spell_casts"],
"properties": {
"spell_casts": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"caster",
"spell",
"effect",
"narrative_description",
"source_refs"
],
"properties": {
"caster": {
"type": "string",
"minLength": 1
},
"spell": {
"type": "string",
"minLength": 1
},
"effect": {
"type": "string",
"minLength": 1
},
"narrative_description": {
"type": "string",
"minLength": 1
},
"source_refs": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["source_id", "start_unit_id", "end_unit_id"],
"properties": {
"source_id": {
"type": "string",
"minLength": 1
},
"start_unit_id": {
"type": "string",
"minLength": 1
},
"end_unit_id": {
"type": "string",
"minLength": 1
}
}
}
}
}
}
}
}
}

View File

@@ -17,6 +17,7 @@ var schemaAssets embed.FS
type ResponseSchemaKey string type ResponseSchemaKey string
const ( const (
DNDSpellsSchemaKey ResponseSchemaKey = "dnd_spells"
TestArtifactSchemaKey ResponseSchemaKey = "test_artifact" TestArtifactSchemaKey ResponseSchemaKey = "test_artifact"
TestValidatorDecisionSchemaKey ResponseSchemaKey = "test_validator_decision" TestValidatorDecisionSchemaKey ResponseSchemaKey = "test_validator_decision"
@@ -34,6 +35,13 @@ type ResponseSchema struct {
} }
var responseSchemaRegistry = map[ResponseSchemaKey]ResponseSchema{ var responseSchemaRegistry = map[ResponseSchemaKey]ResponseSchema{
DNDSpellsSchemaKey: mustLoadResponseSchema(
DNDSpellsSchemaKey,
"notarius.dnd.spells",
schemaVersionV1,
"notarius_dnd_spells_v1",
"assets/schemas/dnd_spells.v1.json",
),
TestArtifactSchemaKey: mustLoadResponseSchema( TestArtifactSchemaKey: mustLoadResponseSchema(
TestArtifactSchemaKey, TestArtifactSchemaKey,
"notarius.test_artifact", "notarius.test_artifact",

View File

@@ -7,8 +7,9 @@ import (
"testing" "testing"
) )
func TestLookupResponseSchemaSucceedsForTestSchemas(t *testing.T) { func TestLookupResponseSchemaSucceedsForRegisteredSchemas(t *testing.T) {
tests := []ResponseSchemaKey{ tests := []ResponseSchemaKey{
DNDSpellsSchemaKey,
TestArtifactSchemaKey, TestArtifactSchemaKey,
TestValidatorDecisionSchemaKey, TestValidatorDecisionSchemaKey,
} }
@@ -50,17 +51,22 @@ func TestMustLookupResponseSchemaPanicsForUnknownKey(t *testing.T) {
func TestRegisteredResponseSchemasSortedByKey(t *testing.T) { func TestRegisteredResponseSchemasSortedByKey(t *testing.T) {
schemas := RegisteredResponseSchemas() schemas := RegisteredResponseSchemas()
if len(schemas) != 2 { if len(schemas) != 3 {
t.Fatalf("expected two schemas, got %d", len(schemas)) t.Fatalf("expected three schemas, got %d", len(schemas))
} }
keys := make([]string, len(schemas)) keys := make([]string, len(schemas))
seen := make(map[ResponseSchemaKey]bool, len(schemas))
for i, schema := range schemas { for i, schema := range schemas {
keys[i] = string(schema.Key) keys[i] = string(schema.Key)
seen[schema.Key] = true
} }
if !sort.StringsAreSorted(keys) { if !sort.StringsAreSorted(keys) {
t.Fatalf("expected sorted keys, got %v", keys) t.Fatalf("expected sorted keys, got %v", keys)
} }
if !seen[DNDSpellsSchemaKey] {
t.Fatalf("registered schemas = %v, want %q", keys, DNDSpellsSchemaKey)
}
} }
func TestResponseSchemaContentIsValidJSON(t *testing.T) { func TestResponseSchemaContentIsValidJSON(t *testing.T) {
@@ -72,10 +78,12 @@ func TestResponseSchemaContentIsValidJSON(t *testing.T) {
} }
func TestResponseSchemaJSONIsMutationSafe(t *testing.T) { func TestResponseSchemaJSONIsMutationSafe(t *testing.T) {
first := MustLookupResponseSchema(TestArtifactSchemaKey) for _, key := range []ResponseSchemaKey{DNDSpellsSchemaKey, TestArtifactSchemaKey} {
t.Run(string(key), func(t *testing.T) {
first := MustLookupResponseSchema(key)
first.JSONSchema[0] = '[' first.JSONSchema[0] = '['
second := MustLookupResponseSchema(TestArtifactSchemaKey) second := MustLookupResponseSchema(key)
if !json.Valid(second.JSONSchema) { if !json.Valid(second.JSONSchema) {
t.Fatalf("schema JSON was mutated: %s", second.JSONSchema) t.Fatalf("schema JSON was mutated: %s", second.JSONSchema)
} }
@@ -85,14 +93,16 @@ func TestResponseSchemaJSONIsMutationSafe(t *testing.T) {
registered := RegisteredResponseSchemas() registered := RegisteredResponseSchemas()
for i := range registered { for i := range registered {
if registered[i].Key == TestArtifactSchemaKey { if registered[i].Key == key {
registered[i].JSONSchema[0] = '[' registered[i].JSONSchema[0] = '['
} }
} }
again := MustLookupResponseSchema(TestArtifactSchemaKey) again := MustLookupResponseSchema(key)
if !json.Valid(again.JSONSchema) || again.JSONSchema[0] == '[' { if !json.Valid(again.JSONSchema) || again.JSONSchema[0] == '[' {
t.Fatalf("registered schema JSON did not use defensive copy") t.Fatalf("registered schema JSON did not use defensive copy")
} }
})
}
} }
func TestResponseSchemaDiagnosticsMapOmitsRawSchemaContent(t *testing.T) { func TestResponseSchemaDiagnosticsMapOmitsRawSchemaContent(t *testing.T) {

View File

@@ -0,0 +1,48 @@
package spells
import (
"encoding/json"
"strings"
"testing"
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
)
func TestLookupResponseSchemaForSpells(t *testing.T) {
schema, ok := llm.LookupResponseSchema(llm.DNDSpellsSchemaKey)
if !ok {
t.Fatalf("LookupResponseSchema(%q) ok = false, want true", llm.DNDSpellsSchemaKey)
}
if schema.ID != "notarius.dnd.spells" {
t.Fatalf("schema.ID = %q, want notarius.dnd.spells", schema.ID)
}
if schema.Version != SchemaVersion {
t.Fatalf("schema.Version = %q, want %q", schema.Version, SchemaVersion)
}
if schema.Name != "notarius_dnd_spells_v1" {
t.Fatalf("schema.Name = %q, want notarius_dnd_spells_v1", schema.Name)
}
if !strings.HasPrefix(schema.SHA256, "sha256:") {
t.Fatalf("schema.SHA256 = %q, want sha256 prefix", schema.SHA256)
}
if !json.Valid(schema.JSONSchema) {
t.Fatalf("schema.JSONSchema is invalid JSON: %s", schema.JSONSchema)
}
}
func TestResponseSchemaDiagnosticsOmitRawSchema(t *testing.T) {
schema := llm.MustLookupResponseSchema(llm.DNDSpellsSchemaKey)
diagnostics := schema.DiagnosticsMap()
if diagnostics["key"] != llm.DNDSpellsSchemaKey {
t.Fatalf("diagnostics[key] = %#v, want %q", diagnostics["key"], llm.DNDSpellsSchemaKey)
}
for _, key := range []string{"id", "version", "name", "sha256"} {
if diagnostics[key] == "" {
t.Fatalf("diagnostics[%q] = %#v, want value", key, diagnostics[key])
}
}
if _, ok := diagnostics["json_schema"]; ok {
t.Fatalf("diagnostics should omit raw schema content: %#v", diagnostics)
}
}