Minimize D&D NPC extraction contracts

This commit is contained in:
2026-07-22 18:51:26 +00:00
parent 14991cf58b
commit a263a0840c
43 changed files with 486 additions and 1395 deletions

View File

@@ -13,9 +13,6 @@
"required": [
"id",
"name",
"aliases",
"description",
"relationships",
"source_refs"
],
"properties": {
@@ -27,35 +24,6 @@
"type": "string",
"minLength": 1
},
"aliases": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
},
"description": {
"type": "string",
"minLength": 1
},
"relationships": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["target", "relationship"],
"properties": {
"target": {
"type": "string",
"minLength": 1
},
"relationship": {
"type": "string",
"minLength": 1
}
}
}
},
"source_refs": {
"type": "array",
"minItems": 1,

View File

@@ -106,29 +106,6 @@ func validate(value dnd.NPCList) error {
if strings.TrimSpace(npc.Name) == "" {
return fmt.Errorf("%s.name must not be empty", prefix)
}
if npc.Aliases == nil {
return fmt.Errorf("%s.aliases must be present", prefix)
}
for aliasIndex, alias := range npc.Aliases {
if strings.TrimSpace(alias) == "" {
return fmt.Errorf("%s.aliases[%d] must not be empty", prefix, aliasIndex)
}
}
if strings.TrimSpace(npc.Description) == "" {
return fmt.Errorf("%s.description must not be empty", prefix)
}
if npc.Relationships == nil {
return fmt.Errorf("%s.relationships must be present", prefix)
}
for relationshipIndex, relationship := range npc.Relationships {
relationshipPrefix := fmt.Sprintf("%s.relationships[%d]", prefix, relationshipIndex)
if strings.TrimSpace(relationship.Target) == "" {
return fmt.Errorf("%s.target must not be empty", relationshipPrefix)
}
if strings.TrimSpace(relationship.Relationship) == "" {
return fmt.Errorf("%s.relationship must not be empty", relationshipPrefix)
}
}
if len(npc.SourceRefs) == 0 {
return fmt.Errorf("%s.source_refs must not be empty", prefix)
}

View File

@@ -17,13 +17,8 @@ import (
func validList() dnd.NPCList {
return dnd.NPCList{NPCs: []dnd.NPC{{
ID: identity.DeriveID("Mira Thorn"),
Name: "Mira Thorn",
Aliases: []string{"The Greencloak"},
Description: "A guarded ranger who watches the northern road.",
Relationships: []dnd.NPCRelationship{{
Target: "Captain Vale", Relationship: "reports to",
}},
ID: identity.DeriveID("Mira Thorn"),
Name: "Mira Thorn",
SourceRefs: []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 1, EndUnitID: 2}},
}}}
}
@@ -86,10 +81,11 @@ func TestCodecStrictlyRejectsMalformedOrUnknownJSON(t *testing.T) {
want string
}{
{name: "unknown top-level", raw: `{"npcs":[],"unexpected":true}`, want: "unknown field"},
{name: "unknown nested", raw: `{"npcs":[{"id":"x","name":"Mira","aliases":[],"description":"desc","relationships":[],"source_refs":[{"source_id":"s","start_unit_id":1,"end_unit_id":1}],"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","aliases":[],"description":"desc","relationships":[],"source_refs":[{"source_id":"s","start_unit_id":0,"end_unit_id":1}]}]}`, want: "start_unit_id"},
{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"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
@@ -103,7 +99,7 @@ func TestCodecStrictlyRejectsMalformedOrUnknownJSON(t *testing.T) {
func TestCodecCandidatePreservesInvalidTypedValues(t *testing.T) {
codec := New()
candidate := dnd.NPCList{NPCs: []dnd.NPC{{Name: "Mira Thorn", Aliases: []string{}, Relationships: []dnd.NPCRelationship{}, SourceRefs: []source.SourceRef{}}}}
candidate := dnd.NPCList{NPCs: []dnd.NPC{{Name: "Mira Thorn", SourceRefs: []source.SourceRef{}}}}
content, err := codec.EncodeCandidate(candidate)
if err != nil || !json.Valid(content) {
t.Fatalf("EncodeCandidate() = %s, %v; want JSON", content, err)
@@ -124,11 +120,8 @@ func TestCodecRejectsEveryRequiredShapeBoundary(t *testing.T) {
value dnd.NPCList
want string
}{
{name: "nil aliases", value: dnd.NPCList{NPCs: []dnd.NPC{{ID: base.ID, Name: base.Name, Description: base.Description, Relationships: []dnd.NPCRelationship{}, SourceRefs: base.SourceRefs}}}, want: "aliases must be present"},
{name: "empty alias", value: dnd.NPCList{NPCs: []dnd.NPC{{ID: base.ID, Name: base.Name, Aliases: []string{" "}, Description: base.Description, Relationships: []dnd.NPCRelationship{}, SourceRefs: base.SourceRefs}}}, want: "aliases[0] must not be empty"},
{name: "nil relationships", value: dnd.NPCList{NPCs: []dnd.NPC{{ID: base.ID, Name: base.Name, Aliases: []string{}, Description: base.Description, SourceRefs: base.SourceRefs}}}, want: "relationships must be present"},
{name: "empty relationship target", value: dnd.NPCList{NPCs: []dnd.NPC{{ID: base.ID, Name: base.Name, Aliases: []string{}, Description: base.Description, Relationships: []dnd.NPCRelationship{{Relationship: "knows"}}, SourceRefs: base.SourceRefs}}}, want: "target must not be empty"},
{name: "empty source refs", value: dnd.NPCList{NPCs: []dnd.NPC{{ID: base.ID, Name: base.Name, Aliases: []string{}, Description: base.Description, Relationships: []dnd.NPCRelationship{}, SourceRefs: []source.SourceRef{}}}}, want: "source_refs must not be empty"},
{name: "blank name", value: dnd.NPCList{NPCs: []dnd.NPC{{ID: base.ID, Name: " ", SourceRefs: base.SourceRefs}}}, want: "name must not be empty"},
{name: "empty source refs", value: dnd.NPCList{NPCs: []dnd.NPC{{ID: base.ID, Name: base.Name, SourceRefs: []source.SourceRef{}}}}, want: "source_refs must not be empty"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {

View File

@@ -3,9 +3,6 @@
{
"id": "npc:sha256:99a16589618a04f535a7d21fdcc71a0b1c05d22f752cd492065b1086d97bc3d7",
"name": "Mira Thorn",
"aliases": ["The Greencloak"],
"description": "A guarded ranger who watches the northern road.",
"relationships": [{"target": "Captain Vale", "relationship": "reports to"}],
"source_refs": [{"source_id": "session-alpha", "start_unit_id": 1, "end_unit_id": 2}]
}
]