Add D&D item event normalization

This commit is contained in:
2026-07-25 22:00:32 +00:00
parent da83510234
commit 5711f8b9e3
6 changed files with 735 additions and 11 deletions

View File

@@ -46,7 +46,7 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
issues := make([]string, 0)
for eventIndex, event := range req.Value.Events {
if itemevents.ValidSourceRefs(index, event.SourceRefs) {
if req.Stage != string(pipeline.StageExtract) || refsFitChunk(req.Chunk, event.SourceRefs) {
if req.Stage != string(pipeline.StageExtract) || refsFitChunk(req.Source, req.Chunk, event.SourceRefs) {
continue
}
}
@@ -55,7 +55,7 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
issues = append(issues, fmt.Sprintf("events[%d].source_refs[%d]: %s", eventIndex, refIndex, diagnostics.Truncate(err.Error())))
continue
}
if req.Stage == string(pipeline.StageExtract) && !chunkContainsRef(req.Chunk, ref) {
if req.Stage == string(pipeline.StageExtract) && !chunkContainsRef(req.Source, req.Chunk, ref) {
issues = append(issues, fmt.Sprintf("events[%d].source_refs[%d]: source reference is outside the current extraction chunk", eventIndex, refIndex))
}
}
@@ -66,26 +66,34 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
return contracts.ValidationResult{Approved: false, ReasonCode: ReasonCode, Message: diagnostics.Aggregate("invalid item event source references", issues)}, nil
}
func refsFitChunk(chunk *source.Chunk, refs []source.SourceRef) bool {
func refsFitChunk(doc *source.SourceDocument, chunk *source.Chunk, refs []source.SourceRef) bool {
for _, ref := range refs {
if !chunkContainsRef(chunk, ref) {
if !chunkContainsRef(doc, chunk, ref) {
return false
}
}
return true
}
func chunkContainsRef(chunk *source.Chunk, ref source.SourceRef) bool {
func chunkContainsRef(doc *source.SourceDocument, chunk *source.Chunk, ref source.SourceRef) bool {
if chunk == nil || ref.SourceID != chunk.SourceID {
return false
}
startFound := false
endFound := false
for _, unit := range chunk.Units {
startFound = startFound || unit.ID == ref.StartUnitID
endFound = endFound || unit.ID == ref.EndUnitID
start, startOK := source.UnitIndex(doc, ref.StartUnitID)
end, endOK := source.UnitIndex(doc, ref.EndUnitID)
if !startOK || !endOK || start > end {
return false
}
return startFound && endFound
units := make(map[int]struct{}, len(chunk.Units))
for _, unit := range chunk.Units {
units[unit.ID] = struct{}{}
}
for position := start; position <= end; position++ {
if _, found := units[doc.Units[position].ID]; !found {
return false
}
}
return true
}
func Spec() pipeline.ValidatorSpec {

View File

@@ -51,6 +51,14 @@ func TestValidatorEnforcesChunkOnlyDuringExtraction(t *testing.T) {
t.Fatalf("out-of-chunk evidence = %#v, %v", result, err)
}
noncontiguousChunk := &source.Chunk{ID: "chunk-1", SourceID: doc.ID, Units: []source.SourceUnit{doc.Units[0], doc.Units[2]}}
spansMissingUnit := validList()
spansMissingUnit.Events[0].SourceRefs = []source.SourceRef{{SourceID: doc.ID, StartUnitID: 1, EndUnitID: 3}}
result, err = New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.ItemEventList]{Stage: string(pipeline.StageExtract), Source: doc, Chunk: noncontiguousChunk, Value: spansMissingUnit})
if err != nil || result.Approved || !strings.Contains(result.Message, "outside the current extraction chunk") {
t.Fatalf("partially contained evidence = %#v, %v", result, err)
}
multiRange := validList()
multiRange.Events[0].SourceRefs = []source.SourceRef{{SourceID: doc.ID, StartUnitID: 1, EndUnitID: 2}, {SourceID: doc.ID, StartUnitID: 3, EndUnitID: 4}}
result, err = New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.ItemEventList]{Source: doc, Value: multiRange})