Finish the D&D audit fixes

This commit is contained in:
2026-07-21 16:08:28 +00:00
parent 3ba2bfd7f6
commit ed36f7d7fd
13 changed files with 74 additions and 22 deletions

View File

@@ -111,7 +111,10 @@ messages before its task and instructions. Identity, transcript, and
campaign-reference messages are ephemeral in the extraction prompts; the scene
prompt marks transcript and campaign references ephemeral, and the NPC registry
is ephemeral where spell and combat prompts use it. Unused shared assets are
neither mounted nor fingerprinted.
neither mounted nor fingerprinted. Universal extraction-evidence and output
policy lives only in the shared extraction assets; package-owned prompt files
retain artifact-specific rules. The scene prompt keeps its separate output rule
because it does not render the extraction-evidence asset.
Schema helpers load embedded JSON Schema with identity and digest metadata,
return defensive copies, and expose a diagnostics map that omits schema bytes.

View File

@@ -15,5 +15,5 @@ attribute relevant nonstandard rulings to the GM or table.
Unmatched actors and targets remain permitted.
For every factual detail in a turn, cite all supporting transcript units in the
turn-level source_refs collection.
Place all supporting transcript ranges for a turn in its turn-level source_refs
collection.

View File

@@ -8,5 +8,4 @@ Relationships must be stated or directly demonstrated by cited transcript
units, not inferred from game lore.
Return aliases and relationships as arrays, including empty arrays when there
are none. Return only NPC records supported by the transcript and preserve
observed display spelling.
are none. Preserve observed display spelling.

View File

@@ -7,9 +7,9 @@ a stable alias or title, or an individually useful distinguishing description.
Exclude human players, transcript speakers, and the GM as out-of-world people,
player characters identified by the player or party references, incidental or
hypothetical name drops, corrected transcription mistakes, characters mentioned
only by reference material, indistinguishable crowds or groups, and temporary
summoned creatures or spell effects without a persistent individual identity.
hypothetical name drops, corrected transcription mistakes, indistinguishable
crowds or groups, and temporary summoned creatures or spell effects without a
persistent individual identity.
Keep each description concise and limited to facts established by the
transcript. Include only explicitly supported aliases and relationships.

View File

@@ -21,10 +21,6 @@ Use the canonical spell-name catalog to select spell names. Do not return a
spell name absent from that catalog, even when it is suggested by general D&D
knowledge or reference material.
Use player, party, and glossary reference material only to clarify source text.
Do not return spells, casters, or effects that are mentioned only in reference
material.
Effects and narrative descriptions are session records, not rules summaries.
Report only mechanics, explanations, and outcomes established by the cited
transcript units. Preserve the table's observed resolution without silently

View File

@@ -1,8 +1,7 @@
Extract Dungeons & Dragons spell-cast artifacts from the provided transcript.
Extract only spell casts that are supported by the transcript. Do not infer
spells from general D&D knowledge or from table chatter that does not identify a
spell being cast.
Do not infer a spell cast from general D&D knowledge or from table chatter that
does not identify a spell being cast.
Describe the session as it was played and adjudicated. The transcript is
authoritative for what happened in this session, even when a table ruling may

View File

@@ -4,5 +4,3 @@ Use only the provided transcript and reference material. Source text may contain
transcription errors, repeated lines, incomplete sentences, and misheard proper
nouns. Reference material, when present, is supporting context only and must not
be treated as a source of extracted events by itself.
Return only valid JSON matching the configured response schema.

View File

@@ -50,7 +50,16 @@ func ReferencePromptInput(slot contracts.ResolvedReferenceSlot) []byte {
if items[i].Digest != items[j].Digest {
return items[i].Digest < items[j].Digest
}
return string(items[i].Content) < string(items[j].Content)
if comparison := bytes.Compare(items[i].Content, items[j].Content); comparison != 0 {
return comparison < 0
}
if items[i].Origin.Type != items[j].Origin.Type {
return items[i].Origin.Type < items[j].Origin.Type
}
if items[i].MediaType != items[j].MediaType {
return items[i].MediaType < items[j].MediaType
}
return items[i].SizeBytes < items[j].SizeBytes
})
if len(items) == 1 {
return append([]byte(nil), items[0].Content...)

View File

@@ -190,6 +190,47 @@ func TestReferencePromptInputRendering(t *testing.T) {
}
}
func TestReferencePromptInputOrdersRenderedMetadataDeterministically(t *testing.T) {
base := contracts.ReferenceItem{
SlotName: "party",
MediaType: "text/plain",
Content: []byte("same content"),
Digest: "sha256:same",
Origin: contracts.ReferenceOrigin{Type: "file", URI: "file:///same.txt"},
SizeBytes: 12,
}
tests := []struct {
name string
first contracts.ReferenceItem
last contracts.ReferenceItem
}{
{
name: "origin type",
first: referenceItemWithMetadata(base, "archive", "text/plain", 12),
last: referenceItemWithMetadata(base, "file", "text/plain", 12),
},
{
name: "media type",
first: referenceItemWithMetadata(base, "file", "application/json", 12),
last: referenceItemWithMetadata(base, "file", "text/plain", 12),
},
{
name: "size",
first: referenceItemWithMetadata(base, "file", "text/plain", 11),
last: referenceItemWithMetadata(base, "file", "text/plain", 12),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
forward := ReferencePromptInput(contracts.ResolvedReferenceSlot{Items: []contracts.ReferenceItem{test.first, test.last}})
reversed := ReferencePromptInput(contracts.ResolvedReferenceSlot{Items: []contracts.ReferenceItem{test.last, test.first}})
if !reflect.DeepEqual(forward, reversed) {
t.Fatalf("rendered bytes depend on insertion order:\nforward=%q\nreversed=%q", forward, reversed)
}
})
}
}
func slotWithContent(name string, content string) contracts.ResolvedReferenceSlot {
return contracts.ResolvedReferenceSlot{
Slot: contracts.ReferenceSlot{Name: name},
@@ -208,3 +249,10 @@ func referenceItem(slotName, uri, digest, content string) contracts.ReferenceIte
Origin: contracts.ReferenceOrigin{URI: uri},
}
}
func referenceItemWithMetadata(base contracts.ReferenceItem, originType, mediaType string, size int64) contracts.ReferenceItem {
base.Origin.Type = originType
base.MediaType = mediaType
base.SizeBytes = size
return base
}

View File

@@ -16,7 +16,7 @@ import (
const (
Key = "extract/dnd/combat-turns/source_relatedness"
WarningReasonCode = "combat_turn_not_near_source"
policy = "dnd.combat_turns.validator.source_relatedness.v1"
policy = "dnd.combat_turns.validator.source_relatedness.v2"
)
type Options struct{}

View File

@@ -87,7 +87,7 @@ func TestValidatorIgnoresReferenceMaterialAndRegistersPolicy(t *testing.T) {
if err != nil || !result.Approved || len(result.Warnings) != 1 {
t.Fatalf("reference-only relatedness = %#v, %v; want warning from transcript-only evidence", result, err)
}
if got := New(Options{}).CheckpointFingerprints(); len(got) != 1 || got[0].Name != "policy" || got[0].Value != policy {
if got := New(Options{}).CheckpointFingerprints(); len(got) != 1 || got[0].Name != "policy" || got[0].Value != "dnd.combat_turns.validator.source_relatedness.v2" {
t.Fatalf("CheckpointFingerprints() = %#v, want relatedness policy", got)
}
if Spec().Key != Key || Spec().ExecutionClass != contracts.ExecutionClassDeterministic {

View File

@@ -15,7 +15,7 @@ import (
const (
Key = "extract/dnd/npcs/source_relatedness"
WarningReasonCode = "npc_not_near_source"
policy = "dnd.npcs.validator.source_relatedness.v1"
policy = "dnd.npcs.validator.source_relatedness.v2"
)
type Options struct{}

View File

@@ -76,7 +76,7 @@ func TestValidatorUsesOnlyTranscriptEvidenceAndRegistersPolicy(t *testing.T) {
if err != nil || len(result.Warnings) != 1 {
t.Fatalf("reference-only relatedness = %#v, %v; want warning", result, err)
}
if got := New(Options{}).CheckpointFingerprints(); len(got) != 1 || got[0].Name != "policy" || got[0].Value != "dnd.npcs.validator.source_relatedness.v1" {
if got := New(Options{}).CheckpointFingerprints(); len(got) != 1 || got[0].Name != "policy" || got[0].Value != "dnd.npcs.validator.source_relatedness.v2" {
t.Fatalf("CheckpointFingerprints() = %#v, want local policy", got)
}
if Spec().ExecutionClass != contracts.ExecutionClassDeterministic {