Move item occurrences to canonical namespace

This commit is contained in:
2026-08-05 20:00:20 +00:00
parent 3dfefd0e14
commit a6e176e160
46 changed files with 342 additions and 337 deletions

View File

@@ -1,5 +1,5 @@
// Package itemevents encodes durable D&D item-event artifacts.
package itemevents
// Package itemoccurrences encodes durable D&D item-occurrence artifacts.
package itemoccurrences
import (
"embed"
@@ -10,7 +10,7 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/candidatejson"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/itemevents"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/itemoccurrences"
)
const (
@@ -86,35 +86,35 @@ func (c *Codec) DecodeCandidate(content []byte) (dnd.ItemOccurrenceList, error)
func validate(value dnd.ItemOccurrenceList) error {
if value.Occurrences == nil {
return fmt.Errorf("events must be present")
return fmt.Errorf("occurrences must be present")
}
for index, event := range value.Occurrences {
for index, occurrence := range value.Occurrences {
prefix := fmt.Sprintf("occurrences[%d]", index)
if strings.TrimSpace(event.ItemID) == "" {
if strings.TrimSpace(occurrence.ItemID) == "" {
return fmt.Errorf("%s.item_id must not be empty", prefix)
}
if strings.TrimSpace(event.Name) == "" {
if strings.TrimSpace(occurrence.Name) == "" {
return fmt.Errorf("%s.name must not be empty", prefix)
}
if !itemevents.SupportedKind(event.Kind) {
if !itemoccurrences.SupportedKind(occurrence.Kind) {
return fmt.Errorf("%s.kind must be supported", prefix)
}
if event.From != "" && strings.TrimSpace(event.From) == "" {
if occurrence.From != "" && strings.TrimSpace(occurrence.From) == "" {
return fmt.Errorf("%s.from must not be empty when present", prefix)
}
if event.To != "" && strings.TrimSpace(event.To) == "" {
if occurrence.To != "" && strings.TrimSpace(occurrence.To) == "" {
return fmt.Errorf("%s.to must not be empty when present", prefix)
}
if !itemevents.ValidHolderCombination(event.Kind, event.From, event.To) {
return fmt.Errorf("%s holders are incompatible with %q", prefix, event.Kind)
if !itemoccurrences.ValidHolderCombination(occurrence.Kind, occurrence.From, occurrence.To) {
return fmt.Errorf("%s holders are incompatible with %q", prefix, occurrence.Kind)
}
if event.Quantity != nil && *event.Quantity < 1 {
if occurrence.Quantity != nil && *occurrence.Quantity < 1 {
return fmt.Errorf("%s.quantity must be positive when present", prefix)
}
if len(event.SourceRefs) == 0 {
if len(occurrence.SourceRefs) == 0 {
return fmt.Errorf("%s.source_refs must contain at least one reference", prefix)
}
for refIndex, ref := range event.SourceRefs {
for refIndex, ref := range occurrence.SourceRefs {
refPrefix := fmt.Sprintf("%s.source_refs[%d]", prefix, refIndex)
if strings.TrimSpace(ref.SourceID) == "" {
return fmt.Errorf("%s.source_id must not be empty", refPrefix)
@@ -135,14 +135,14 @@ func cloneList(value dnd.ItemOccurrenceList) dnd.ItemOccurrenceList {
return dnd.ItemOccurrenceList{}
}
cloned := dnd.ItemOccurrenceList{Occurrences: make([]dnd.ItemOccurrence, len(value.Occurrences))}
for index, event := range value.Occurrences {
cloned.Occurrences[index] = event
if event.Quantity != nil {
quantity := *event.Quantity
for index, occurrence := range value.Occurrences {
cloned.Occurrences[index] = occurrence
if occurrence.Quantity != nil {
quantity := *occurrence.Quantity
cloned.Occurrences[index].Quantity = &quantity
}
if event.SourceRefs != nil {
cloned.Occurrences[index].SourceRefs = append([]source.SourceRef(nil), event.SourceRefs...)
if occurrence.SourceRefs != nil {
cloned.Occurrences[index].SourceRefs = append([]source.SourceRef(nil), occurrence.SourceRefs...)
}
}
return cloned

View File

@@ -1,4 +1,4 @@
package itemevents
package itemoccurrences
import (
"bytes"
@@ -93,10 +93,10 @@ func TestCodecRejectsStrictJSONAndApprovedBoundaries(t *testing.T) {
}{
{"malformed", `{`, "decode dnd item occurrence list"},
{"unknown top level", `{"occurrences":[],"unexpected":true}`, "unknown field"},
{"unknown event field", strings.Replace(validJSON, `"to":"party"`, `"to":"party","unexpected":true`, 1), "unknown field"},
{"unknown occurrence field", strings.Replace(validJSON, `"to":"party"`, `"to":"party","unexpected":true`, 1), "unknown field"},
{"unknown reference field", strings.Replace(validJSON, `"end_unit_id":1`, `"end_unit_id":1,"unexpected":true`, 1), "unknown field"},
{"trailing", `{"occurrences":[]} {}`, "multiple JSON values"},
{"missing list", `{}`, "events must be present"},
{"missing list", `{}`, "occurrences must be present"},
{"zero quantity", strings.Replace(validJSON, `"to":"party"`, `"quantity":0,"to":"party"`, 1), "quantity must be positive"},
{"negative quantity", strings.Replace(validJSON, `"to":"party"`, `"quantity":-1,"to":"party"`, 1), "quantity must be positive"},
{"party transfer", strings.Replace(validJSON, `"kind":"acquired","to":"party"`, `"kind":"transferred","from":"party","to":"Borin"`, 1), "holders are incompatible"},
@@ -125,7 +125,7 @@ func TestCodecDeepCopiesBoundaryValuesAndMetadata(t *testing.T) {
t.Fatal(err)
}
if decoded.Occurrences[1].Quantity == value.Occurrences[1].Quantity || &decoded.Occurrences[1].SourceRefs[0] == &value.Occurrences[1].SourceRefs[0] {
t.Fatal("DecodeCandidate() retained caller-owned event fields")
t.Fatal("DecodeCandidate() retained caller-owned occurrence fields")
}
*decoded.Occurrences[1].Quantity = 99
decoded.Occurrences[1].SourceRefs[0].SourceID = "changed"

View File

@@ -1,4 +1,4 @@
package itemevents
package itemoccurrences
import (
"sort"
@@ -42,12 +42,12 @@ func canonicalizeResponse(response *extractionResponse, order shared.SourceRefOr
}
}
func canonicalizeItemOccurrence(event *itemOccurrenceResponse, order shared.SourceRefOrder, sourceID string) (int, bool) {
if event == nil {
func canonicalizeItemOccurrence(occurrence *itemOccurrenceResponse, order shared.SourceRefOrder, sourceID string) (int, bool) {
if occurrence == nil {
return 0, false
}
refs := order.Canonicalize(itemOccurrenceSourceRefs(event.SourceRefs, sourceID))
event.SourceRefs = itemOccurrenceResponseRefs(refs)
refs := order.Canonicalize(itemOccurrenceSourceRefs(occurrence.SourceRefs, sourceID))
occurrence.SourceRefs = itemOccurrenceResponseRefs(refs)
return order.EarliestValid(refs)
}
@@ -56,19 +56,19 @@ func canonicalItemOccurrenceList(response extractionResponse, sourceID string, r
return dnd.ItemOccurrenceList{}
}
occurrences := make([]dnd.ItemOccurrence, 0, len(response.Occurrences))
for _, event := range response.Occurrences {
item, found := registry.LookupID(event.ItemID)
if !found || item.Name != event.Name {
for _, occurrence := range response.Occurrences {
item, found := registry.LookupID(occurrence.ItemID)
if !found || item.Name != occurrence.Name {
continue
}
occurrences = append(occurrences, dnd.ItemOccurrence{
ItemID: event.ItemID,
Name: event.Name,
Kind: dnd.ItemOccurrenceKind(event.Kind),
Quantity: cloneQuantity(event.Quantity),
From: event.From,
To: event.To,
SourceRefs: itemOccurrenceSourceRefs(event.SourceRefs, sourceID),
ItemID: occurrence.ItemID,
Name: occurrence.Name,
Kind: dnd.ItemOccurrenceKind(occurrence.Kind),
Quantity: cloneQuantity(occurrence.Quantity),
From: occurrence.From,
To: occurrence.To,
SourceRefs: itemOccurrenceSourceRefs(occurrence.SourceRefs, sourceID),
})
}
return dnd.ItemOccurrenceList{Occurrences: occurrences}

View File

@@ -1,5 +1,5 @@
// Package itemevents extracts source-grounded D&D item occurrences.
package itemevents
// Package itemoccurrences extracts source-grounded D&D item occurrences.
package itemoccurrences
import (
"context"
@@ -13,14 +13,14 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
)
const Key = "dnd/item-events"
const Key = "dnd/item-occurrences"
const (
ItemRegistryReferenceSlot = itemregistry.ReferenceSlot
ItemRegistryMaxBytes = itemregistry.MaxBytes
)
const mappingPolicy = "dnd.item_events.extract_mapping.v1"
const mappingPolicy = "dnd.item_occurrences.extract_mapping.v1"
var requiredCapabilities = []string{
"chunks",
@@ -28,7 +28,7 @@ var requiredCapabilities = []string{
}
var providedCapabilities = []string{
"dnd.item_events",
"dnd.item_occurrences",
}
var referenceSlotDescriptions = shared.ReferenceSlotDescriptions{

View File

@@ -1,4 +1,4 @@
package itemevents
package itemoccurrences
import (
"context"

View File

@@ -1,4 +1,4 @@
package itemevents
package itemoccurrences
type extractionResponse struct {
Occurrences []itemOccurrenceResponse `json:"occurrences"`

View File

@@ -1,4 +1,4 @@
package itemevents
package itemoccurrences
import (
"fmt"
@@ -14,7 +14,7 @@ import (
const promptAssetRoot = "assets/prompts"
var promptAssetManifest = shared.PromptAssetManifest{
ModuleDir: "dnd.item_events",
ModuleDir: "dnd.item_occurrences",
ModuleFiles: []promptfs.ModulePromptFile{
{Name: "prompt.yaml", Path: "prompts/prompt.yaml"},
{Name: "instructions.md", Path: "prompts/instructions.md"},
@@ -30,9 +30,9 @@ var promptAssetManifest = shared.PromptAssetManifest{
}
func moduleAssetFS() (fs.FS, error) {
assets, err := fs.Sub(rootassets.FS(), "dnd/item-events")
assets, err := fs.Sub(rootassets.FS(), "dnd/item-occurrences")
if err != nil {
return nil, fmt.Errorf("scope item-event assets: %w", err)
return nil, fmt.Errorf("scope item-occurrence assets: %w", err)
}
return assets, nil
}
@@ -51,7 +51,7 @@ func RegisterPromptAssets(registry *llm.AssetRegistry) error {
}
schemas, err := fs.Sub(assets, "schemas")
if err != nil {
return fmt.Errorf("scope item-event schemas: %w", err)
return fmt.Errorf("scope item-occurrence schemas: %w", err)
}
return registry.RegisterSchemaFS(schemas, ".")
}

View File

@@ -1,4 +1,4 @@
package itemevents
package itemoccurrences
import (
"context"
@@ -21,17 +21,17 @@ func TestPromptAssetsPrepareItemOccurrencePrompt(t *testing.T) {
t.Fatal(err)
}
options = append(options, promptkit.WithProfiles(promptkit.OpenAICompatibleProfile(promptkit.OpenAICompatibleProfileConfig{
ID: "item-events-test-profile", Endpoint: "http://127.0.0.1:1/v1", Model: "item-events-test-model",
ID: "item-occurrences-test-profile", Endpoint: "http://127.0.0.1:1/v1", Model: "item-occurrences-test-model",
})))
engine, err := promptkit.NewEngine(promptkit.Config{Timeout: time.Second}, options...)
if err != nil {
t.Fatal(err)
}
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{
PromptID: PromptID, PromptVersion: SchemaVersion, ProfileID: "item-events-test-profile",
PromptID: PromptID, PromptVersion: SchemaVersion, ProfileID: "item-occurrences-test-profile",
Inputs: map[string]promptkit.ArtifactRef{
"transcript": promptkit.InlineWithURI("file:///session.json", `{"units":[{"sentinel":"item-event-transcript"}]}`),
"players": promptkit.Inline("item-event-player"),
"transcript": promptkit.InlineWithURI("file:///session.json", `{"units":[{"sentinel":"item-occurrence-transcript"}]}`),
"players": promptkit.Inline("item-occurrence-player"),
"party": promptkit.Inline(" "),
"glossary": promptkit.Inline(" "),
"item_registry": promptkit.Inline(`{"items":[{"id":"item:sha256:test","name":"Torch"}]}`),

View File

@@ -1,4 +1,4 @@
package itemevents
package itemoccurrences
import (
"reflect"

View File

@@ -1,9 +1,9 @@
package itemevents
package itemoccurrences
import "gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
const (
PromptID = "dnd.item_events"
PromptID = "dnd.item_occurrences"
ResponseSchemaKey = llm.ResponseSchemaKey("dnd_item_occurrences_llm")
ResponseSchemaID = "notarius.dnd.item_occurrences.llm"
ResponseSchemaName = "notarius_dnd_item_occurrences_llm_v1"

View File

@@ -1,4 +1,4 @@
package itemevents
package itemoccurrences
import (
"bytes"
@@ -39,11 +39,11 @@ func TestResponseSchemaIsStrictlyStructuralAndPrivate(t *testing.T) {
value map[string]any
}{
{"missing occurrences", map[string]any{}},
{"missing event name", map[string]any{"occurrences": []any{withoutField(responseEvent(), "name")}}},
{"missing nullable field", map[string]any{"occurrences": []any{withoutField(responseEvent(), "quantity")}}},
{"unknown event field", map[string]any{"occurrences": []any{withField(responseEvent(), "extra", true)}}},
{"unknown reference field", map[string]any{"occurrences": []any{withField(responseEvent(), "source_refs", []any{map[string]any{"start_segment": 1, "end_segment": 1, "extra": true}})}}},
{"noninteger range", map[string]any{"occurrences": []any{withField(responseEvent(), "source_refs", []any{map[string]any{"start_segment": 1.5, "end_segment": 1}})}}},
{"missing occurrence name", map[string]any{"occurrences": []any{withoutField(responseOccurrence(), "name")}}},
{"missing nullable field", map[string]any{"occurrences": []any{withoutField(responseOccurrence(), "quantity")}}},
{"unknown occurrence field", map[string]any{"occurrences": []any{withField(responseOccurrence(), "extra", true)}}},
{"unknown reference field", map[string]any{"occurrences": []any{withField(responseOccurrence(), "source_refs", []any{map[string]any{"start_segment": 1, "end_segment": 1, "extra": true}})}}},
{"noninteger range", map[string]any{"occurrences": []any{withField(responseOccurrence(), "source_refs", []any{map[string]any{"start_segment": 1.5, "end_segment": 1}})}}},
} {
t.Run(test.name, func(t *testing.T) {
content, err := json.Marshal(test.value)
@@ -66,7 +66,7 @@ func TestResponseSchemaIsStrictlyStructuralAndPrivate(t *testing.T) {
}
}
func responseEvent() map[string]any {
func responseOccurrence() map[string]any {
return map[string]any{
"item_id": "item:sha256:test", "name": "Ring", "kind": "acquired", "quantity": nil, "from": nil, "to": "party", "source_refs": []any{},
}

View File

@@ -1,4 +1,4 @@
package itemevents
package itemoccurrences
import (
"context"
@@ -26,7 +26,7 @@ func extractionRequest() contracts.TypedExtractionRequest {
Chunk: chunk,
SourceInput: contracts.NewLLMInputMaterial("source", chunk.MediaType, chunk.Content, "sha256:chunk", "file:///session-alpha.json"),
SessionID: "session-123",
LLMProfile: "profile-item-events",
LLMProfile: "profile-item-occurrences",
}
}

View File

@@ -1,6 +1,6 @@
// Package itemevents owns canonical ordering and durable domain rules for D&D
// item-event artifacts.
package itemevents
// Package itemoccurrences owns canonical ordering and durable domain rules for D&D
// item-occurrence artifacts.
package itemoccurrences
import (
"strconv"
@@ -90,7 +90,7 @@ func ValidSourceRefs(index source.DocumentIndex, refs []source.SourceRef) bool {
return true
}
// Less defines the canonical event order. Invalid source references remain
// Less defines the canonical occurrence order. Invalid source references remain
// comparable through SourceRefOrder's literal fallback so malformed candidates
// are still safe to sort and diagnose.
func Less(order shared.SourceRefOrder, left, right dnd.ItemOccurrence) bool {
@@ -126,7 +126,7 @@ func Less(order shared.SourceRefOrder, left, right dnd.ItemOccurrence) bool {
return sourceRefsLess(order, order.Canonicalize(left.SourceRefs), order.Canonicalize(right.SourceRefs))
}
// ExactEqual reports whether events are exact duplicates after their display
// ExactEqual reports whether occurrences are exact duplicates after their display
// fields and evidence have been canonicalized for the supplied source order.
func ExactEqual(order shared.SourceRefOrder, left, right dnd.ItemOccurrence) bool {
if left.ItemID != right.ItemID || DisplayValue(left.Name) != DisplayValue(right.Name) || left.Kind != right.Kind ||
@@ -142,21 +142,21 @@ func ExactEqual(order shared.SourceRefOrder, left, right dnd.ItemOccurrence) boo
// ExactIdentity returns a collision-safe duplicate key after display and
// evidence canonicalization. It is intended for callers that have already
// decided the event is eligible for duplicate handling.
func ExactIdentity(order shared.SourceRefOrder, event dnd.ItemOccurrence) string {
// decided the occurrence is eligible for duplicate handling.
func ExactIdentity(order shared.SourceRefOrder, occurrence dnd.ItemOccurrence) string {
var key strings.Builder
writeKeyString(&key, event.ItemID)
writeKeyString(&key, DisplayValue(event.Name))
writeKeyString(&key, string(event.Kind))
writeKeyString(&key, DisplayValue(event.From))
writeKeyString(&key, DisplayValue(event.To))
if event.Quantity == nil {
writeKeyString(&key, occurrence.ItemID)
writeKeyString(&key, DisplayValue(occurrence.Name))
writeKeyString(&key, string(occurrence.Kind))
writeKeyString(&key, DisplayValue(occurrence.From))
writeKeyString(&key, DisplayValue(occurrence.To))
if occurrence.Quantity == nil {
key.WriteByte('0')
} else {
key.WriteByte('1')
writeKeyInt(&key, *event.Quantity)
writeKeyInt(&key, *occurrence.Quantity)
}
for _, ref := range order.Canonicalize(event.SourceRefs) {
for _, ref := range order.Canonicalize(occurrence.SourceRefs) {
writeKeyString(&key, ref.SourceID)
writeKeyInt(&key, ref.StartUnitID)
writeKeyInt(&key, ref.EndUnitID)

View File

@@ -1,4 +1,4 @@
package itemevents
package itemoccurrences
import (
"sort"
@@ -136,13 +136,13 @@ func TestSourceReferenceHelpers(t *testing.T) {
func TestLessSortsMalformedReferencesDeterministically(t *testing.T) {
order := testOrder()
events := []dnd.ItemOccurrence{{ItemID: "item", Name: "A", Kind: dnd.ItemOccurrenceKindDiscovered, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 999, EndUnitID: 999}}},
occurrences := []dnd.ItemOccurrence{{ItemID: "item", Name: "A", Kind: dnd.ItemOccurrenceKindDiscovered, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 999, EndUnitID: 999}}},
{ItemID: "item", Name: "A", Kind: dnd.ItemOccurrenceKindDiscovered, SourceRefs: []source.SourceRef{{SourceID: "other", StartUnitID: 1, EndUnitID: 1}}},
{ItemID: "item", Name: "A", Kind: dnd.ItemOccurrenceKindDiscovered, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 10, EndUnitID: 10}}},
}
sort.SliceStable(events, func(left, right int) bool { return Less(order, events[left], events[right]) })
if events[0].SourceRefs[0].StartUnitID != 10 || events[1].SourceRefs[0].SourceID != "other" || events[2].SourceRefs[0].StartUnitID != 999 {
t.Fatalf("canonical sort = %#v", events)
sort.SliceStable(occurrences, func(left, right int) bool { return Less(order, occurrences[left], occurrences[right]) })
if occurrences[0].SourceRefs[0].StartUnitID != 10 || occurrences[1].SourceRefs[0].SourceID != "other" || occurrences[2].SourceRefs[0].StartUnitID != 999 {
t.Fatalf("canonical sort = %#v", occurrences)
}
}
@@ -150,28 +150,31 @@ func testOrder() shared.SourceRefOrder {
return shared.NewSourceRefOrder(&source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 10}, {ID: 20}, {ID: 30}}})
}
func withName(event dnd.ItemOccurrence, value string) dnd.ItemOccurrence {
event.Name = value
return event
func withName(occurrence dnd.ItemOccurrence, value string) dnd.ItemOccurrence {
occurrence.Name = value
return occurrence
}
func withKind(event dnd.ItemOccurrence, value dnd.ItemOccurrenceKind) dnd.ItemOccurrence {
event.Kind = value
return event
func withKind(occurrence dnd.ItemOccurrence, value dnd.ItemOccurrenceKind) dnd.ItemOccurrence {
occurrence.Kind = value
return occurrence
}
func withFrom(event dnd.ItemOccurrence, value string) dnd.ItemOccurrence {
event.From = value
return event
func withFrom(occurrence dnd.ItemOccurrence, value string) dnd.ItemOccurrence {
occurrence.From = value
return occurrence
}
func withTo(event dnd.ItemOccurrence, value string) dnd.ItemOccurrence {
event.To = value
return event
func withTo(occurrence dnd.ItemOccurrence, value string) dnd.ItemOccurrence {
occurrence.To = value
return occurrence
}
func withoutTo(event dnd.ItemOccurrence) dnd.ItemOccurrence { event.To = ""; return event }
func withQuantity(event dnd.ItemOccurrence, value *int) dnd.ItemOccurrence {
event.Quantity = value
return event
func withoutTo(occurrence dnd.ItemOccurrence) dnd.ItemOccurrence {
occurrence.To = ""
return occurrence
}
func withRefs(event dnd.ItemOccurrence, value []source.SourceRef) dnd.ItemOccurrence {
event.SourceRefs = value
return event
func withQuantity(occurrence dnd.ItemOccurrence, value *int) dnd.ItemOccurrence {
occurrence.Quantity = value
return occurrence
}
func withRefs(occurrence dnd.ItemOccurrence, value []source.SourceRef) dnd.ItemOccurrence {
occurrence.SourceRefs = value
return occurrence
}

View File

@@ -1,5 +1,5 @@
// Package itemevents normalizes merged D&D item-event candidates.
package itemevents
// Package itemoccurrences normalizes merged D&D item-occurrence candidates.
package itemoccurrences
import (
"context"
@@ -10,15 +10,15 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
itemeventmodel "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/itemevents"
itemoccurrencemodel "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/itemoccurrences"
itemregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/items/registry"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
)
const (
Key = "dnd/item-events"
normalizationPolicy = "dnd.item_events.normalize.v1"
Key = "dnd/item-occurrences"
normalizationPolicy = "dnd.item_occurrences.normalize.v1"
NormalizationPolicy = normalizationPolicy
ReasonCodeNameCanonicalized = "item_occurrence_name_canonicalized"
@@ -111,7 +111,7 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
}
type normalizedRecord struct {
event dnd.ItemOccurrence
occurrence dnd.ItemOccurrence
inputIndex int
}
@@ -122,44 +122,44 @@ func normalizeList(input dnd.ItemOccurrenceList, index source.DocumentIndex, ord
records := make([]normalizedRecord, len(input.Occurrences))
warnings := make([]contracts.Warning, 0)
for index, inputEvent := range input.Occurrences {
event, changedFields, found, refsChanged := normalizeEvent(inputEvent, order, registry)
records[index] = normalizedRecord{event: event, inputIndex: index}
for index, inputOccurrence := range input.Occurrences {
occurrence, changedFields, found, refsChanged := normalizeOccurrence(inputOccurrence, order, registry)
records[index] = normalizedRecord{occurrence: occurrence, inputIndex: index}
if len(changedFields) != 0 {
warnings = append(warnings, contracts.Warning{
Scope: eventScope(index),
Scope: occurrenceScope(index),
ReasonCode: ReasonCodeNameCanonicalized,
Message: fmt.Sprintf("input index %d: normalized display whitespace in %s", index,
diagnostics.Aggregate("fields", changedFields)),
})
}
if found && inputEvent.Name != event.Name {
warnings = append(warnings, contracts.Warning{Scope: eventScope(index), ReasonCode: ReasonCodeNameCanonicalized,
Message: fmt.Sprintf("input index %d: item name canonicalized from %s to %s", index, diagnostics.Quote(inputEvent.Name), diagnostics.Quote(event.Name))})
if found && inputOccurrence.Name != occurrence.Name {
warnings = append(warnings, contracts.Warning{Scope: occurrenceScope(index), ReasonCode: ReasonCodeNameCanonicalized,
Message: fmt.Sprintf("input index %d: item name canonicalized from %s to %s", index, diagnostics.Quote(inputOccurrence.Name), diagnostics.Quote(occurrence.Name))})
}
if !found {
warnings = append(warnings, contracts.Warning{Scope: eventScope(index), ReasonCode: ReasonCodeUnknownItemID,
Message: fmt.Sprintf("input index %d: item ID %s is not in the supplied registry", index, diagnostics.Quote(inputEvent.ItemID))})
warnings = append(warnings, contracts.Warning{Scope: occurrenceScope(index), ReasonCode: ReasonCodeUnknownItemID,
Message: fmt.Sprintf("input index %d: item ID %s is not in the supplied registry", index, diagnostics.Quote(inputOccurrence.ItemID))})
}
if refsChanged {
warnings = append(warnings, contracts.Warning{
Scope: eventScope(index),
Scope: occurrenceScope(index),
ReasonCode: ReasonCodeSourceRefsNormalized,
Message: fmt.Sprintf("input index %d: source references normalized (original count %d, final count %d)",
index, len(inputEvent.SourceRefs), len(event.SourceRefs)),
index, len(inputOccurrence.SourceRefs), len(occurrence.SourceRefs)),
})
}
}
sort.SliceStable(records, func(left, right int) bool {
return itemeventmodel.Less(order, records[left].event, records[right].event)
return itemoccurrencemodel.Less(order, records[left].occurrence, records[right].occurrence)
})
for position, record := range records {
if position == record.inputIndex {
continue
}
warnings = append(warnings, contracts.Warning{
Scope: eventScope(record.inputIndex),
Scope: occurrenceScope(record.inputIndex),
ReasonCode: ReasonCodeOccurrencesReordered,
Message: fmt.Sprintf("input index %d moved to normalized position %d", record.inputIndex, position),
})
@@ -170,8 +170,8 @@ func normalizeList(input dnd.ItemOccurrenceList, index source.DocumentIndex, ord
return dnd.ItemOccurrenceList{Occurrences: output}, diagnostics.LimitWarnings(warnings, "item_occurrences", ReasonCodeWarningsOmitted)
}
func normalizeEvent(input dnd.ItemOccurrence, order shared.SourceRefOrder, registry *itemregistry.Registry) (dnd.ItemOccurrence, []string, bool, bool) {
output := cloneEvent(input)
func normalizeOccurrence(input dnd.ItemOccurrence, order shared.SourceRefOrder, registry *itemregistry.Registry) (dnd.ItemOccurrence, []string, bool, bool) {
output := cloneOccurrence(input)
canonical, found := registry.LookupID(input.ItemID)
if found {
output.Name = canonical.Name
@@ -184,17 +184,17 @@ func normalizeEvent(input dnd.ItemOccurrence, order shared.SourceRefOrder, regis
{name: "from", value: &output.From},
{name: "to", value: &output.To},
} {
trimmed := itemeventmodel.DisplayValue(*field.value)
trimmed := itemoccurrencemodel.DisplayValue(*field.value)
if *field.value != trimmed {
*field.value = trimmed
changedFields = append(changedFields, field.name)
}
}
output.SourceRefs = order.Canonicalize(input.SourceRefs)
return output, changedFields, found, !itemeventmodel.SourceRefsEqual(input.SourceRefs, output.SourceRefs)
return output, changedFields, found, !itemoccurrencemodel.SourceRefsEqual(input.SourceRefs, output.SourceRefs)
}
func cloneEvent(input dnd.ItemOccurrence) dnd.ItemOccurrence {
func cloneOccurrence(input dnd.ItemOccurrence) dnd.ItemOccurrence {
output := input
if input.Quantity != nil {
quantity := *input.Quantity
@@ -220,11 +220,11 @@ func collapseDuplicates(records []normalizedRecord, index source.DocumentIndex,
groups := make([]duplicateGroup, 0)
groupByKey := make(map[string]int)
for recordIndex, record := range records {
if !itemeventmodel.ValidSourceRefs(index, record.event.SourceRefs) {
if !itemoccurrencemodel.ValidSourceRefs(index, record.occurrence.SourceRefs) {
keep[recordIndex] = true
continue
}
key := itemeventmodel.ExactIdentity(order, record.event)
key := itemoccurrencemodel.ExactIdentity(order, record.occurrence)
groupIndex, exists := groupByKey[key]
if !exists {
groupByKey[key] = len(groups)
@@ -238,7 +238,7 @@ func collapseDuplicates(records []normalizedRecord, index source.DocumentIndex,
output := make([]dnd.ItemOccurrence, 0, len(records))
for recordIndex, record := range records {
if keep[recordIndex] {
output = append(output, cloneEvent(record.event))
output = append(output, cloneOccurrence(record.occurrence))
}
}
warnings := make([]contracts.Warning, 0)
@@ -256,14 +256,14 @@ func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
issues[index] = fmt.Sprintf("removed input index %d", removedIndex)
}
return contracts.Warning{
Scope: eventScope(retainedIndex),
Scope: occurrenceScope(retainedIndex),
ReasonCode: ReasonCodeDuplicateCollapsed,
Message: diagnostics.Aggregate(
fmt.Sprintf("duplicate item occurrence collapsed; retained input index %d", retainedIndex), issues),
}
}
func eventScope(index int) string { return fmt.Sprintf("occurrences[%d]", index) }
func occurrenceScope(index int) string { return fmt.Sprintf("occurrences[%d]", index) }
func ModuleSpec() pipeline.ModuleSpec {
return pipeline.ModuleSpec{

View File

@@ -1,4 +1,4 @@
package itemevents
package itemoccurrences
import (
"context"

View File

@@ -4,7 +4,7 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns"
enemyeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/enemyevents"
itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents"
itemoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemoccurrences"
itemregistryextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemregistry"
locationoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences"
locationextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationregistry"
@@ -14,7 +14,7 @@ import (
spellextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
combatnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/combatturns"
enemyeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/enemyevents"
itemeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemevents"
itemoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemoccurrences"
itemregistrynormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemregistry"
locationoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationoccurrences"
locationnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationregistry"
@@ -31,11 +31,11 @@ import (
enemyeventshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/enemyevents/shape"
enemyeventrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/enemyevents/source_refs"
enemyeventrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/enemyevents/source_relatedness"
itemeventinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/invariants"
itemeventregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/registry"
itemeventshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/shape"
itemeventrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/source_refs"
itemeventrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/source_relatedness"
itemoccurrenceinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/invariants"
itemoccurrenceregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/registry"
itemoccurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/shape"
itemoccurrencerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/source_refs"
itemoccurrencerelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/source_relatedness"
itemidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemregistry/identity"
itemshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemregistry/shape"
itemrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemregistry/source_refs"
@@ -185,29 +185,29 @@ func registerDefaultChains(registry *pipeline.ValidatorChainRegistry) error {
{name: "item occurrences validator chain", register: func() error {
return registry.Register(pipeline.ValidatorChainMapping{
Stage: pipeline.StageExtract,
Module: itemeventextract.Key,
Module: itemoccurrenceextract.Key,
Validators: []pipeline.ModuleBinding{
pipeline.Binding(validjson.Key),
pipeline.Binding(itemeventshape.Key),
pipeline.Binding(itemeventregistry.Key),
pipeline.Binding(itemeventrefs.Key),
pipeline.Binding(itemoccurrenceshape.Key),
pipeline.Binding(itemoccurrenceregistry.Key),
pipeline.Binding(itemoccurrencerefs.Key),
pipeline.Binding(validjsonschema.Key),
pipeline.Binding(itemeventrelatedness.Key),
pipeline.Binding(itemoccurrencerelatedness.Key),
},
})
}},
{name: "item occurrences normalize validator chain", register: func() error {
return registry.Register(pipeline.ValidatorChainMapping{
Stage: pipeline.StageNormalize,
Module: itemeventnormalize.Key,
Module: itemoccurrencenormalize.Key,
Validators: []pipeline.ModuleBinding{
pipeline.Binding(validjson.Key),
pipeline.Binding(itemeventshape.Key),
pipeline.Binding(itemeventregistry.Key),
pipeline.Binding(itemeventinvariants.Key),
pipeline.Binding(itemeventrefs.Key),
pipeline.Binding(itemoccurrenceshape.Key),
pipeline.Binding(itemoccurrenceregistry.Key),
pipeline.Binding(itemoccurrenceinvariants.Key),
pipeline.Binding(itemoccurrencerefs.Key),
pipeline.Binding(validjsonschema.Key),
pipeline.Binding(itemeventrelatedness.Key),
pipeline.Binding(itemoccurrencerelatedness.Key),
},
})
}},

View File

@@ -7,7 +7,7 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/chunk/scenes"
combatcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/combatturns"
enemyeventcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/enemyevents"
itemeventcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemevents"
itemoccurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemoccurrences"
itemregistrycodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemregistry"
locationoccurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationoccurrences"
locationcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationregistry"
@@ -17,7 +17,7 @@ import (
spellcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/spells"
combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns"
enemyeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/enemyevents"
itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents"
itemoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemoccurrences"
itemregistryextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemregistry"
locationoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences"
locationextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationregistry"
@@ -27,7 +27,7 @@ import (
spellextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
combatnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/combatturns"
enemyeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/enemyevents"
itemeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemevents"
itemoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemoccurrences"
itemregistrynormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemregistry"
locationoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationoccurrences"
locationnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationregistry"
@@ -47,7 +47,9 @@ func registerModules(registries pipeline.Registries) error {
{name: "npc registry codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, npccodec.New()) }},
{name: "combat turns codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, combatcodec.New()) }},
{name: "enemy events codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, enemyeventcodec.New()) }},
{name: "item occurrences codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, itemeventcodec.New()) }},
{name: "item occurrences codec", register: func() error {
return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, itemoccurrencecodec.New())
}},
{name: "item registry codec", register: func() error {
return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, itemregistrycodec.New())
}},
@@ -64,7 +66,7 @@ func registerModules(registries pipeline.Registries) error {
{name: "npc registry extractor", register: func() error { return npcextract.Register(registries.Extractors) }},
{name: "combat turns extractor", register: func() error { return combatextract.Register(registries.Extractors) }},
{name: "enemy events extractor", register: func() error { return enemyeventextract.Register(registries.Extractors) }},
{name: "item occurrences extractor", register: func() error { return itemeventextract.Register(registries.Extractors) }},
{name: "item occurrences extractor", register: func() error { return itemoccurrenceextract.Register(registries.Extractors) }},
{name: "item registry extractor", register: func() error { return itemregistryextract.Register(registries.Extractors) }},
{name: "npc occurrences extractor", register: func() error { return occurrenceextract.Register(registries.Extractors) }},
{name: "scene descriptions extractor", register: func() error { return scenedescriptionextract.Register(registries.Extractors) }},
@@ -104,7 +106,7 @@ func registerModules(registries pipeline.Registries) error {
{name: "npc registry normalizer", register: func() error { return npcnormalize.Register(registries.Normalizers) }},
{name: "combat turns normalizer", register: func() error { return combatnormalize.Register(registries.Normalizers) }},
{name: "enemy events normalizer", register: func() error { return enemyeventnormalize.Register(registries.Normalizers) }},
{name: "item occurrences normalizer", register: func() error { return itemeventnormalize.Register(registries.Normalizers) }},
{name: "item occurrences normalizer", register: func() error { return itemoccurrencenormalize.Register(registries.Normalizers) }},
{name: "item registry normalizer", register: func() error { return itemregistrynormalize.Register(registries.Normalizers) }},
{name: "npc occurrences normalizer", register: func() error { return occurrencenormalize.Register(registries.Normalizers) }},
{name: "scene descriptions normalizer", register: func() error { return scenedescriptionnormalize.Register(registries.Normalizers) }},
@@ -152,7 +154,7 @@ func registerPromptAssets(assets *llm.AssetRegistry) error {
{name: "npc normalization prompt assets", register: func() error { return npcnormalize.RegisterPromptAssets(assets) }},
{name: "combat turns prompt assets", register: func() error { return combatextract.RegisterPromptAssets(assets) }},
{name: "enemy events prompt assets", register: func() error { return enemyeventextract.RegisterPromptAssets(assets) }},
{name: "item occurrences prompt assets", register: func() error { return itemeventextract.RegisterPromptAssets(assets) }},
{name: "item occurrences prompt assets", register: func() error { return itemoccurrenceextract.RegisterPromptAssets(assets) }},
{name: "item registry prompt assets", register: func() error { return itemregistryextract.RegisterPromptAssets(assets) }},
{name: "item registry normalization prompt assets", register: func() error { return itemregistrynormalize.RegisterPromptAssets(assets) }},
{name: "npc occurrences prompt assets", register: func() error { return occurrenceextract.RegisterPromptAssets(assets) }},

View File

@@ -10,7 +10,7 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns"
enemyeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/enemyevents"
itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents"
itemoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemoccurrences"
itemregistryextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemregistry"
locationoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences"
locationextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationregistry"
@@ -51,7 +51,7 @@ func TestExtractionPromptComposition(t *testing.T) {
}{
{name: "npcs", promptID: npcextract.PromptID, promptVersion: npcextract.SchemaVersion, inputs: commonInputs, suffixGroups: [][]string{{evidenceSentinel}}},
{name: "locations", promptID: locationextract.PromptID, promptVersion: locationextract.SchemaVersion, inputs: commonInputs, suffixGroups: [][]string{{evidenceSentinel}}},
{name: "item occurrences", promptID: itemeventextract.PromptID, promptVersion: itemeventextract.SchemaVersion, inputs: withPromptInputs(commonInputs, map[string]promptkit.ArtifactRef{
{name: "item occurrences", promptID: itemoccurrenceextract.PromptID, promptVersion: itemoccurrenceextract.SchemaVersion, inputs: withPromptInputs(commonInputs, map[string]promptkit.ArtifactRef{
"item_registry": promptkit.Inline(`{"items":[{"id":"item-registry-sentinel","name":"Torch"}]}`),
}), suffixGroups: [][]string{{evidenceSentinel}, {"item-registry-sentinel"}}},
{name: "item registry", promptID: itemregistryextract.PromptID, promptVersion: itemregistryextract.SchemaVersion, inputs: commonInputs, suffixGroups: [][]string{{evidenceSentinel}}},

View File

@@ -14,7 +14,7 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns"
enemyeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/enemyevents"
itemeventextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemevents"
itemoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemoccurrences"
itemregistryextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemregistry"
locationoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences"
locationextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationregistry"
@@ -24,7 +24,7 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
combatnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/combatturns"
enemyeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/enemyevents"
itemeventnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemevents"
itemoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemoccurrences"
itemregistrynormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemregistry"
locationoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationoccurrences"
locationnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationregistry"
@@ -53,7 +53,7 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
"dnd.npc_registry/prompt.yaml",
"dnd.combat_turns/prompt.yaml",
"dnd.enemy_events/prompt.yaml",
"dnd.item_events/prompt.yaml",
"dnd.item_occurrences/prompt.yaml",
"dnd.item_registry/prompt.yaml",
"dnd.item_registry.normalize/prompt.yaml",
"dnd.npc_occurrences/prompt.yaml",
@@ -86,8 +86,8 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
t.Fatalf("entity reconciliation schema asset = %v, want registered shared schema", err)
}
assertContainsKeys(t, "chunkers", registries.Chunkers.RegisteredKeys(), []string{"dnd/scenes"})
assertContainsKeys(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells", npcextract.Key, combatextract.Key, enemyeventextract.Key, itemeventextract.Key, itemregistryextract.Key, occurrenceextract.Key, scenedescriptionextract.Key, locationextract.Key, locationoccurrenceextract.Key})
assertContainsKeys(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{spellnormalize.Key, npcnormalize.Key, combatnormalize.Key, enemyeventnormalize.Key, itemeventnormalize.Key, itemregistrynormalize.Key, occurrencenormalize.Key, scenedescriptionnormalize.Key, locationnormalize.Key, locationoccurrencenormalize.Key, pipeline.DefaultNormalizeModule})
assertContainsKeys(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells", npcextract.Key, combatextract.Key, enemyeventextract.Key, itemoccurrenceextract.Key, itemregistryextract.Key, occurrenceextract.Key, scenedescriptionextract.Key, locationextract.Key, locationoccurrenceextract.Key})
assertContainsKeys(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{spellnormalize.Key, npcnormalize.Key, combatnormalize.Key, enemyeventnormalize.Key, itemoccurrencenormalize.Key, itemregistrynormalize.Key, occurrencenormalize.Key, scenedescriptionnormalize.Key, locationnormalize.Key, locationoccurrencenormalize.Key, pipeline.DefaultNormalizeModule})
assertContainsArtifactKinds(t, registries.ArtifactCodecs.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemOccurrenceListKind, dnd.ItemRegistryKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationRegistryKind, dnd.LocationOccurrenceListKind})
assertContainsArtifactKinds(t, registries.ArtifactEvidence.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemOccurrenceListKind, dnd.ItemRegistryKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationRegistryKind, dnd.LocationOccurrenceListKind})
assertContainsArtifactKinds(t, registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemOccurrenceListKind, dnd.ItemRegistryKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationRegistryKind, dnd.LocationOccurrenceListKind})
@@ -95,7 +95,7 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(npcnormalize.Key), []contracts.ArtifactKind{dnd.NPCRegistryKind})
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(combatnormalize.Key), []contracts.ArtifactKind{dnd.CombatTurnListKind})
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(enemyeventnormalize.Key), []contracts.ArtifactKind{dnd.EnemyEventListKind})
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(itemeventnormalize.Key), []contracts.ArtifactKind{dnd.ItemOccurrenceListKind})
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(itemoccurrencenormalize.Key), []contracts.ArtifactKind{dnd.ItemOccurrenceListKind})
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(itemregistrynormalize.Key), []contracts.ArtifactKind{dnd.ItemRegistryKind})
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(occurrencenormalize.Key), []contracts.ArtifactKind{dnd.NPCOccurrenceListKind})
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(scenedescriptionnormalize.Key), []contracts.ArtifactKind{dnd.SceneDescriptionListKind})
@@ -119,10 +119,10 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
"extract/dnd/enemy-events/source_refs",
"extract/dnd/enemy-events/source_relatedness",
"normalize/dnd/enemy-events/invariants",
"extract/dnd/item-events/shape",
"extract/dnd/item-events/source_refs",
"extract/dnd/item-events/source_relatedness",
"normalize/dnd/item-events/invariants",
"extract/dnd/item-occurrences/shape",
"extract/dnd/item-occurrences/source_refs",
"extract/dnd/item-occurrences/source_relatedness",
"normalize/dnd/item-occurrences/invariants",
"extract/dnd/item-registry/shape", "normalize/dnd/item-registry/identity", "extract/dnd/item-registry/source_refs", "extract/dnd/item-registry/source_relatedness",
"extract/dnd/npc-occurrences/shape",
"extract/dnd/npc-occurrences/registry",
@@ -232,25 +232,25 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
}
itemOccurrenceExtractChain := []pipeline.ModuleBinding{
pipeline.Binding("generic/valid_json"),
pipeline.Binding("extract/dnd/item-events/shape"),
pipeline.Binding("extract/dnd/item-events/registry"),
pipeline.Binding("extract/dnd/item-events/source_refs"),
pipeline.Binding("extract/dnd/item-occurrences/shape"),
pipeline.Binding("extract/dnd/item-occurrences/registry"),
pipeline.Binding("extract/dnd/item-occurrences/source_refs"),
pipeline.Binding("generic/valid_json_schema"),
pipeline.Binding("extract/dnd/item-events/source_relatedness"),
pipeline.Binding("extract/dnd/item-occurrences/source_relatedness"),
}
itemOccurrenceNormalizeChain := []pipeline.ModuleBinding{
pipeline.Binding("generic/valid_json"),
pipeline.Binding("extract/dnd/item-events/shape"),
pipeline.Binding("extract/dnd/item-events/registry"),
pipeline.Binding("normalize/dnd/item-events/invariants"),
pipeline.Binding("extract/dnd/item-events/source_refs"),
pipeline.Binding("extract/dnd/item-occurrences/shape"),
pipeline.Binding("extract/dnd/item-occurrences/registry"),
pipeline.Binding("normalize/dnd/item-occurrences/invariants"),
pipeline.Binding("extract/dnd/item-occurrences/source_refs"),
pipeline.Binding("generic/valid_json_schema"),
pipeline.Binding("extract/dnd/item-events/source_relatedness"),
pipeline.Binding("extract/dnd/item-occurrences/source_relatedness"),
}
if got := registries.ValidatorChains.Validators(pipeline.StageExtract, itemeventextract.Key); !reflect.DeepEqual(got, itemOccurrenceExtractChain) {
if got := registries.ValidatorChains.Validators(pipeline.StageExtract, itemoccurrenceextract.Key); !reflect.DeepEqual(got, itemOccurrenceExtractChain) {
t.Fatalf("item occurrence extract validator chain = %#v, want %#v", got, itemOccurrenceExtractChain)
}
if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, itemeventnormalize.Key); !reflect.DeepEqual(got, itemOccurrenceNormalizeChain) {
if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, itemoccurrencenormalize.Key); !reflect.DeepEqual(got, itemOccurrenceNormalizeChain) {
t.Fatalf("item occurrence normalize validator chain = %#v, want %#v", got, itemOccurrenceNormalizeChain)
}
itemRegistryExtractChain := []pipeline.ModuleBinding{
@@ -370,8 +370,8 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
if !npcRegistrySlot.Required || !enemyNormalizeRegistrySlot.Required || npcRegistrySlot.MaxBytes != enemyNormalizeRegistrySlot.MaxBytes || !reflect.DeepEqual(npcRegistrySlot.AcceptedMediaTypes, enemyNormalizeRegistrySlot.AcceptedMediaTypes) || !reflect.DeepEqual(npcRegistrySlot.AcceptedArtifactKinds, enemyNormalizeRegistrySlot.AcceptedArtifactKinds) {
t.Fatalf("enemy-event NPC registry slots disagree: %#v / %#v", enemyEventExtractSpec.ReferenceSlots, enemyEventNormalizeSpec.ReferenceSlots)
}
itemOccurrenceExtractSpec, itemOccurrenceExtractOK := registries.Extractors.Spec(itemeventextract.Key)
itemOccurrenceNormalizeSpec, itemOccurrenceNormalizeOK := registries.Normalizers.Spec(itemeventnormalize.Key)
itemOccurrenceExtractSpec, itemOccurrenceExtractOK := registries.Extractors.Spec(itemoccurrenceextract.Key)
itemOccurrenceNormalizeSpec, itemOccurrenceNormalizeOK := registries.Normalizers.Spec(itemoccurrencenormalize.Key)
if !itemOccurrenceExtractOK || itemOccurrenceExtractSpec.ArtifactKind != dnd.ItemOccurrenceListKind || !itemOccurrenceNormalizeOK || itemOccurrenceNormalizeSpec.ArtifactKind != dnd.ItemOccurrenceListKind || itemOccurrenceNormalizeSpec.Stage != pipeline.StageNormalize {
t.Fatalf("item occurrence specs = %#v / %#v, present = %t / %t", itemOccurrenceExtractSpec, itemOccurrenceNormalizeSpec, itemOccurrenceExtractOK, itemOccurrenceNormalizeOK)
}

View File

@@ -12,11 +12,11 @@ import (
enemyeventshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/enemyevents/shape"
enemyeventrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/enemyevents/source_refs"
enemyeventrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/enemyevents/source_relatedness"
itemeventinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/invariants"
itemeventregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/registry"
itemeventshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/shape"
itemeventrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/source_refs"
itemeventrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/source_relatedness"
itemoccurrenceinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/invariants"
itemoccurrenceregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/registry"
itemoccurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/shape"
itemoccurrencerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/source_refs"
itemoccurrencerelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/source_relatedness"
itemidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemregistry/identity"
itemshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemregistry/shape"
itemrefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemregistry/source_refs"
@@ -70,11 +70,11 @@ func registerValidators(registries pipeline.Registries) error {
{name: "enemy event source references validator", register: func() error { return enemyeventrefs.Register(registries.Validators) }},
{name: "enemy event source relatedness validator", register: func() error { return enemyeventrelatedness.Register(registries.Validators) }},
{name: "enemy event normalized invariants validator", register: func() error { return enemyeventinvariants.Register(registries.Validators) }},
{name: "item occurrence shape validator", register: func() error { return itemeventshape.Register(registries.Validators) }},
{name: "item occurrence registry validator", register: func() error { return itemeventregistry.Register(registries.Validators) }},
{name: "item occurrence source references validator", register: func() error { return itemeventrefs.Register(registries.Validators) }},
{name: "item occurrence source relatedness validator", register: func() error { return itemeventrelatedness.Register(registries.Validators) }},
{name: "item occurrence normalized invariants validator", register: func() error { return itemeventinvariants.Register(registries.Validators) }},
{name: "item occurrence shape validator", register: func() error { return itemoccurrenceshape.Register(registries.Validators) }},
{name: "item occurrence registry validator", register: func() error { return itemoccurrenceregistry.Register(registries.Validators) }},
{name: "item occurrence source references validator", register: func() error { return itemoccurrencerefs.Register(registries.Validators) }},
{name: "item occurrence source relatedness validator", register: func() error { return itemoccurrencerelatedness.Register(registries.Validators) }},
{name: "item occurrence normalized invariants validator", register: func() error { return itemoccurrenceinvariants.Register(registries.Validators) }},
{name: "item shape validator", register: func() error { return itemshape.Register(registries.Validators) }},
{name: "item identity validator", register: func() error { return itemidentity.Register(registries.Validators) }},
{name: "item source references validator", register: func() error { return itemrefs.Register(registries.Validators) }},

View File

@@ -1,4 +1,4 @@
// Package invariants validates normalized D&D item-event artifacts.
// Package invariants validates normalized D&D item-occurrence artifacts.
package invariants
import (
@@ -9,16 +9,16 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
itemeventmodel "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/itemevents"
itemoccurrencemodel "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/itemoccurrences"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
itemeventshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/shape"
itemoccurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/shape"
)
const (
Key = "normalize/dnd/item-events/invariants"
ReasonCode = "invalid_normalized_item_event_invariants"
policy = "dnd.item_events.normalize_invariants.v1"
Key = "normalize/dnd/item-occurrences/invariants"
ReasonCode = "invalid_normalized_item_occurrence_invariants"
policy = "dnd.item_occurrences.normalize_invariants.v1"
)
type Options struct{}
@@ -43,10 +43,10 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
return contracts.ValidationResult{Approved: true}, nil
}
// Validate checks only invariants introduced by item-event normalization.
// Validate checks only invariants introduced by item-occurrence normalization.
// Shape and source-reference failures remain owned by their earlier validators.
func Validate(doc *source.SourceDocument, value dnd.ItemOccurrenceList) error {
if itemeventshape.Validate(value) != nil {
if itemoccurrenceshape.Validate(value) != nil {
return nil
}
index := source.NewDocumentIndex(doc)
@@ -63,44 +63,44 @@ func Validate(doc *source.SourceDocument, value dnd.ItemOccurrenceList) error {
func issuesFor(order shared.SourceRefOrder, value dnd.ItemOccurrenceList) []string {
issues := make([]string, 0)
seenIdentity := make(map[string]int)
for eventIndex, event := range value.Occurrences {
prefix := fmt.Sprintf("occurrences[%d]", eventIndex)
if event.Name != itemeventmodel.DisplayValue(event.Name) {
for occurrenceIndex, occurrence := range value.Occurrences {
prefix := fmt.Sprintf("occurrences[%d]", occurrenceIndex)
if occurrence.Name != itemoccurrencemodel.DisplayValue(occurrence.Name) {
issues = append(issues, prefix+".name is not display-normalized")
}
if event.From != itemeventmodel.DisplayValue(event.From) {
if occurrence.From != itemoccurrencemodel.DisplayValue(occurrence.From) {
issues = append(issues, prefix+".from is not display-normalized")
}
if event.To != itemeventmodel.DisplayValue(event.To) {
if occurrence.To != itemoccurrencemodel.DisplayValue(occurrence.To) {
issues = append(issues, prefix+".to is not display-normalized")
}
for refIndex := 1; refIndex < len(event.SourceRefs); refIndex++ {
previous := event.SourceRefs[refIndex-1]
current := event.SourceRefs[refIndex]
for refIndex := 1; refIndex < len(occurrence.SourceRefs); refIndex++ {
previous := occurrence.SourceRefs[refIndex-1]
current := occurrence.SourceRefs[refIndex]
if order.Less(current, previous) {
issues = append(issues, fmt.Sprintf("%s.source_refs are not in canonical order at index %d", prefix, refIndex))
} else if current == previous {
issues = append(issues, fmt.Sprintf("%s.source_refs[%d] duplicates the previous reference", prefix, refIndex))
}
}
key := itemeventmodel.ExactIdentity(order, event)
key := itemoccurrencemodel.ExactIdentity(order, occurrence)
if previous, exists := seenIdentity[key]; exists {
issues = append(issues, fmt.Sprintf("%s duplicates item occurrence %d under normalized identity", prefix, previous))
} else {
seenIdentity[key] = eventIndex
seenIdentity[key] = occurrenceIndex
}
}
for eventIndex := 1; eventIndex < len(value.Occurrences); eventIndex++ {
if itemeventmodel.Less(order, value.Occurrences[eventIndex], value.Occurrences[eventIndex-1]) {
issues = append(issues, fmt.Sprintf("occurrences[%d] is out of canonical order", eventIndex))
for occurrenceIndex := 1; occurrenceIndex < len(value.Occurrences); occurrenceIndex++ {
if itemoccurrencemodel.Less(order, value.Occurrences[occurrenceIndex], value.Occurrences[occurrenceIndex-1]) {
issues = append(issues, fmt.Sprintf("occurrences[%d] is out of canonical order", occurrenceIndex))
}
}
return issues
}
func sourceRefsValid(index source.DocumentIndex, value dnd.ItemOccurrenceList) bool {
for _, event := range value.Occurrences {
if !itemeventmodel.ValidSourceRefs(index, event.SourceRefs) {
for _, occurrence := range value.Occurrences {
if !itemoccurrencemodel.ValidSourceRefs(index, occurrence.SourceRefs) {
return false
}
}

View File

@@ -45,7 +45,7 @@ func TestValidateRejectsOwnedNormalizedInvariants(t *testing.T) {
{name: "list order", mutate: func(value *dnd.ItemOccurrenceList) {
value.Occurrences = []dnd.ItemOccurrence{value.Occurrences[1], value.Occurrences[0]}
}, want: "out of canonical order"},
{name: "duplicate event", mutate: func(value *dnd.ItemOccurrenceList) {
{name: "duplicate occurrence", mutate: func(value *dnd.ItemOccurrenceList) {
value.Occurrences = append(value.Occurrences, value.Occurrences[0])
}, want: "duplicates item occurrence"},
}

View File

@@ -10,11 +10,11 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
itemregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/items/registry"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
occurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/shape"
occurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/shape"
)
const (
Key = "extract/dnd/item-events/registry"
Key = "extract/dnd/item-occurrences/registry"
ReasonCode = "invalid_item_occurrence_registry"
policy = "dnd.item_occurrences.validator.registry.v1"
)

View File

@@ -1,4 +1,4 @@
// Package shape validates required D&D item-event candidate fields.
// Package shape validates required D&D item-occurrence candidate fields.
package shape
import (
@@ -9,14 +9,14 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/itemevents"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/itemoccurrences"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
)
const (
Key = "extract/dnd/item-events/shape"
ReasonCode = "invalid_item_event_shape"
policy = "dnd.item_events.shape.v1"
Key = "extract/dnd/item-occurrences/shape"
ReasonCode = "invalid_item_occurrence_shape"
policy = "dnd.item_occurrences.shape.v1"
)
type Options struct{}
@@ -41,7 +41,7 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
return contracts.ValidationResult{Approved: true}, nil
}
// Validate returns one bounded error for every owned item-event shape issue.
// Validate returns one bounded error for every owned item-occurrence shape issue.
func Validate(value dnd.ItemOccurrenceList) error {
issues := issuesFor(value)
if len(issues) == 0 {
@@ -55,23 +55,23 @@ func issuesFor(value dnd.ItemOccurrenceList) []string {
return []string{"occurrences must be present"}
}
issues := make([]string, 0)
for index, event := range value.Occurrences {
for index, occurrence := range value.Occurrences {
prefix := fmt.Sprintf("occurrences[%d]", index)
if strings.TrimSpace(event.ItemID) == "" {
issues = append(issues, prefix+".item_id must not be empty: "+diagnostics.Quote(event.ItemID))
if strings.TrimSpace(occurrence.ItemID) == "" {
issues = append(issues, prefix+".item_id must not be empty: "+diagnostics.Quote(occurrence.ItemID))
}
if strings.TrimSpace(event.Name) == "" {
issues = append(issues, prefix+".name must not be empty: "+diagnostics.Quote(event.Name))
if strings.TrimSpace(occurrence.Name) == "" {
issues = append(issues, prefix+".name must not be empty: "+diagnostics.Quote(occurrence.Name))
}
if !itemevents.SupportedKind(event.Kind) {
issues = append(issues, prefix+".kind is unsupported: "+diagnostics.Quote(string(event.Kind)))
} else if !itemevents.ValidHolderCombination(event.Kind, event.From, event.To) {
issues = append(issues, prefix+".from and .to are incompatible with "+diagnostics.Quote(string(event.Kind)))
if !itemoccurrences.SupportedKind(occurrence.Kind) {
issues = append(issues, prefix+".kind is unsupported: "+diagnostics.Quote(string(occurrence.Kind)))
} else if !itemoccurrences.ValidHolderCombination(occurrence.Kind, occurrence.From, occurrence.To) {
issues = append(issues, prefix+".from and .to are incompatible with "+diagnostics.Quote(string(occurrence.Kind)))
}
if event.Quantity != nil && *event.Quantity < 1 {
if occurrence.Quantity != nil && *occurrence.Quantity < 1 {
issues = append(issues, prefix+".quantity must be positive when present")
}
if len(event.SourceRefs) == 0 {
if len(occurrence.SourceRefs) == 0 {
issues = append(issues, prefix+".source_refs must contain at least one reference")
}
}

View File

@@ -13,7 +13,7 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
)
func TestValidatorAcceptsEveryEventRuleAndNormalizableWhitespace(t *testing.T) {
func TestValidatorAcceptsEveryOccurrenceRuleAndNormalizableWhitespace(t *testing.T) {
quantity := 1
value := dnd.ItemOccurrenceList{Occurrences: []dnd.ItemOccurrence{{ItemID: "item", Name: " Hidden Cache ", Kind: dnd.ItemOccurrenceKindDiscovered, From: " ", To: " ", SourceRefs: refs(1, 1)},
{ItemID: "item", Name: "Gold Pieces", Kind: dnd.ItemOccurrenceKindAcquired, Quantity: &quantity, To: " party ", SourceRefs: refs(2, 2)},
@@ -40,7 +40,7 @@ func TestValidatorRejectsOwnedSemanticBoundaries(t *testing.T) {
value dnd.ItemOccurrenceList
want string
}{
{"missing events", dnd.ItemOccurrenceList{}, "occurrences must be present"},
{"missing occurrences", dnd.ItemOccurrenceList{}, "occurrences must be present"},
{"blank name", listWith(dnd.ItemOccurrence{ItemID: "item", Name: " \t", Kind: dnd.ItemOccurrenceKindDiscovered, SourceRefs: refs(1, 1)}), "name must not be empty"},
{"unsupported kind", listWith(dnd.ItemOccurrence{ItemID: "item", Name: "Ring", Kind: "unknown", SourceRefs: refs(1, 1)}), "kind is unsupported"},
{"discovered holder", listWith(dnd.ItemOccurrence{ItemID: "item", Name: "Ring", Kind: dnd.ItemOccurrenceKindDiscovered, From: "Aria", SourceRefs: refs(1, 1)}), "incompatible"},
@@ -99,8 +99,8 @@ func validList() dnd.ItemOccurrenceList {
return dnd.ItemOccurrenceList{Occurrences: []dnd.ItemOccurrence{{ItemID: "item", Name: "Ring", Kind: dnd.ItemOccurrenceKindAcquired, To: "party", SourceRefs: refs(1, 1)}}}
}
func listWith(event dnd.ItemOccurrence) dnd.ItemOccurrenceList {
return dnd.ItemOccurrenceList{Occurrences: []dnd.ItemOccurrence{event}}
func listWith(occurrence dnd.ItemOccurrence) dnd.ItemOccurrenceList {
return dnd.ItemOccurrenceList{Occurrences: []dnd.ItemOccurrence{occurrence}}
}
func refs(start, end int) []source.SourceRef {

View File

@@ -1,4 +1,4 @@
// Package sourcerefs validates D&D item-event transcript evidence.
// Package sourcerefs validates D&D item-occurrence transcript evidence.
package sourcerefs
import (
@@ -10,13 +10,13 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
itemeventshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/shape"
itemoccurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/shape"
)
const (
Key = "extract/dnd/item-events/source_refs"
ReasonCode = "invalid_item_event_source_references"
policy = "dnd.item_events.source_refs.v1"
Key = "extract/dnd/item-occurrences/source_refs"
ReasonCode = "invalid_item_occurrence_source_references"
policy = "dnd.item_occurrences.source_refs.v1"
)
type Options struct{}
@@ -38,7 +38,7 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
if req.Stage == string(pipeline.StageExtract) && req.Chunk == nil {
return contracts.ValidationResult{}, fmt.Errorf("item occurrence source-reference validator requires the current extraction chunk")
}
if itemeventshape.Validate(req.Value) != nil {
if itemoccurrenceshape.Validate(req.Value) != nil {
return contracts.ValidationResult{Approved: true}, nil
}
index := source.NewDocumentIndex(req.Source)
@@ -47,14 +47,14 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
coverage = newChunkCoverage(req.Chunk)
}
issues := make([]string, 0)
for eventIndex, event := range req.Value.Occurrences {
for refIndex, ref := range event.SourceRefs {
for occurrenceIndex, occurrence := range req.Value.Occurrences {
for refIndex, ref := range occurrence.SourceRefs {
if err := index.ValidateRef(ref); err != nil {
issues = append(issues, fmt.Sprintf("occurrences[%d].source_refs[%d]: %s", eventIndex, refIndex, diagnostics.Truncate(err.Error())))
issues = append(issues, fmt.Sprintf("occurrences[%d].source_refs[%d]: %s", occurrenceIndex, refIndex, diagnostics.Truncate(err.Error())))
continue
}
if coverage != nil && !coverage.contains(req.Source, ref) {
issues = append(issues, fmt.Sprintf("occurrences[%d].source_refs[%d]: source reference is outside the current extraction chunk", eventIndex, refIndex))
issues = append(issues, fmt.Sprintf("occurrences[%d].source_refs[%d]: source reference is outside the current extraction chunk", occurrenceIndex, refIndex))
}
}
}

View File

@@ -1,4 +1,4 @@
// Package sourcerelatedness warns when item-event evidence does not mention its item.
// Package sourcerelatedness warns when item-occurrence evidence does not mention its item.
package sourcerelatedness
import (
@@ -10,14 +10,14 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
itemeventshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemevents/shape"
itemoccurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/shape"
)
const (
Key = "extract/dnd/item-events/source_relatedness"
WarningReasonCode = "item_event_source_unrelated"
OmittedReasonCode = "item_event_relatedness_warnings_omitted"
policy = "dnd.item_events.source_relatedness.v1"
Key = "extract/dnd/item-occurrences/source_relatedness"
WarningReasonCode = "item_occurrence_source_unrelated"
OmittedReasonCode = "item_occurrence_relatedness_warnings_omitted"
policy = "dnd.item_occurrences.source_relatedness.v1"
)
type Options struct{}
@@ -38,7 +38,7 @@ func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
// Validate performs advisory source grounding only. Shape and range failures
// are intentionally left to their blocking owners.
func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.ItemOccurrenceList]) (contracts.ValidationResult, error) {
if itemeventshape.Validate(req.Value) != nil {
if itemoccurrenceshape.Validate(req.Value) != nil {
return contracts.ValidationResult{Approved: true}, nil
}
resolver, err := shared.NewCitationResolver(req.Source)
@@ -47,20 +47,20 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
}
warnings := make([]contracts.Warning, 0)
for index, event := range req.Value.Occurrences {
citedText, err := resolver.CitedText(event.SourceRefs)
if err != nil || shared.ContainsTokenSequence(citedText, event.Name) {
for index, occurrence := range req.Value.Occurrences {
citedText, err := resolver.CitedText(occurrence.SourceRefs)
if err != nil || shared.ContainsTokenSequence(citedText, occurrence.Name) {
continue
}
warnings = append(warnings, contracts.Warning{
Scope: fmt.Sprintf("occurrences[%d]", index),
ReasonCode: WarningReasonCode,
Message: fmt.Sprintf("item occurrence name %s was not found in cited source text", diagnostics.Quote(event.Name)),
Message: fmt.Sprintf("item occurrence name %s was not found in cited source text", diagnostics.Quote(occurrence.Name)),
})
}
return contracts.ValidationResult{
Approved: true,
Warnings: diagnostics.LimitWarnings(warnings, "item_events", OmittedReasonCode),
Warnings: diagnostics.LimitWarnings(warnings, "item_occurrences", OmittedReasonCode),
}, nil
}

View File

@@ -49,13 +49,13 @@ func TestValidatorDefersMalformedAndUnreadableEvidence(t *testing.T) {
func TestValidatorBoundsWarningsAndRegistersPolicy(t *testing.T) {
count := diagnostics.MaxWarnings + 5
events := make([]dnd.ItemOccurrence, count)
for index := range events {
events[index] = dnd.ItemOccurrence{ItemID: "item", Name: "missing item", Kind: dnd.ItemOccurrenceKindDiscovered, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}
occurrences := make([]dnd.ItemOccurrence, count)
for index := range occurrences {
occurrences[index] = dnd.ItemOccurrence{ItemID: "item", Name: "missing item", Kind: dnd.ItemOccurrenceKindDiscovered, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}
}
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.ItemOccurrenceList]{
Source: &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 1, Text: "nothing useful"}}},
Value: dnd.ItemOccurrenceList{Occurrences: events},
Value: dnd.ItemOccurrenceList{Occurrences: occurrences},
})
if err != nil || !result.Approved || len(result.Warnings) != diagnostics.MaxWarnings || result.Warnings[len(result.Warnings)-1].ReasonCode != OmittedReasonCode {
t.Fatalf("Validate() = %#v, %v", result, err)

View File

@@ -30,9 +30,9 @@ pipelines:
lane: registry
artifacts:
occurrences:
extract: dnd/item-events
extract: dnd/item-occurrences
merge: appendorder
normalize: dnd/item-events
normalize: dnd/item-occurrences
`)
if err != nil {
t.Fatal(err)
@@ -51,7 +51,7 @@ pipelines:
- id: record-occurrences
artifacts:
occurrences:
extract: dnd/item-events
extract: dnd/item-occurrences
`, "record-occurrences", "record-occurrences", 1))
if err != nil {
t.Fatal(err)

View File

@@ -1,7 +1,7 @@
{
"metadata": {
"id": "item-events-session",
"title": "Synthetic D&D item-event session"
"id": "item-occurrences-session",
"title": "Synthetic D&D item-occurrence session"
},
"segments": [
{"id": 1, "start": 0, "end": 4, "speaker": "DM", "text": "An ancient coin rests in the chest."},