Improve D&D validation reliability
This commit is contained in:
@@ -16,7 +16,7 @@ import (
|
||||
const (
|
||||
Key = "extract/dnd/item-occurrences/registry"
|
||||
ReasonCode = "invalid_item_occurrence_registry"
|
||||
policy = "dnd.item_occurrences.validator.registry.v1"
|
||||
policy = "dnd.item_occurrences.validator.registry.v2"
|
||||
)
|
||||
|
||||
type Options struct{}
|
||||
@@ -76,27 +76,33 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
return contracts.ValidationResult{}, fmt.Errorf("resolve item registry: %w", err)
|
||||
}
|
||||
if !registry.Bound() {
|
||||
return rejection([]string{"item registry reference is required"}), nil
|
||||
var corrections diagnostics.Corrections
|
||||
corrections.Add("registry", "Use only contextual item names from the supplied item registry; omit an occurrence that cannot be matched unambiguously.", "")
|
||||
return rejection([]string{"item registry reference is required"}, corrections), nil
|
||||
}
|
||||
issues := make([]string, 0)
|
||||
var corrections diagnostics.Corrections
|
||||
for index, occurrence := range req.Value.Occurrences {
|
||||
record := fmt.Sprintf("Affected %s occurrence for item %s %s.", diagnostics.Quote(string(occurrence.Kind)), diagnostics.Quote(occurrence.Name), diagnostics.SourceRange(occurrence.SourceRefs))
|
||||
item, ok := registry.LookupID(occurrence.ItemID)
|
||||
if !ok {
|
||||
issues = append(issues, fmt.Sprintf("occurrences[%d].item_id is not in the item registry: %s", index, diagnostics.Quote(occurrence.ItemID)))
|
||||
corrections.Add("registry", "Use only contextual item names from the supplied item registry; omit an occurrence that cannot be matched unambiguously.", record)
|
||||
continue
|
||||
}
|
||||
if occurrence.Name != item.Name {
|
||||
issues = append(issues, fmt.Sprintf("occurrences[%d] does not match registry item %s", index, diagnostics.Quote(occurrence.ItemID)))
|
||||
corrections.Add("canonical-name", "Use the exact contextual item name supplied by the registry.", record+" Use registry name "+diagnostics.Quote(item.Name)+".")
|
||||
}
|
||||
}
|
||||
if len(issues) == 0 {
|
||||
return contracts.ValidationResult{Approved: true}, nil
|
||||
}
|
||||
return rejection(issues), nil
|
||||
return rejection(issues, corrections), nil
|
||||
}
|
||||
|
||||
func rejection(issues []string) contracts.ValidationResult {
|
||||
return contracts.ValidationResult{Approved: false, ReasonCode: ReasonCode, Message: diagnostics.Aggregate("invalid item occurrence registry", issues), CorrectionGuidance: "Return item occurrences using contextual item names that match an item in the supplied registry; omit occurrences that cannot be matched unambiguously."}
|
||||
func rejection(issues []string, corrections diagnostics.Corrections) contracts.ValidationResult {
|
||||
return contracts.ValidationResult{Approved: false, ReasonCode: ReasonCode, Message: diagnostics.Aggregate("invalid item occurrence registry", issues), CorrectionGuidance: corrections.Guidance("Correct every rejected item occurrence and return the complete replacement occurrence list")}
|
||||
}
|
||||
func Spec() pipeline.ValidatorSpec {
|
||||
return pipeline.ValidatorSpec{Key: Key, ExecutionClass: contracts.ExecutionClassDeterministic}
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
const (
|
||||
Key = "extract/dnd/item-occurrences/shape"
|
||||
ReasonCode = "invalid_item_occurrence_shape"
|
||||
policy = "dnd.item_occurrences.shape.v2"
|
||||
policy = "dnd.item_occurrences.shape.v3"
|
||||
)
|
||||
|
||||
type Options struct{}
|
||||
@@ -57,20 +57,12 @@ func Validate(value dnd.ItemOccurrenceList) error {
|
||||
}
|
||||
|
||||
type validationAssessment struct {
|
||||
operatorIssues []string
|
||||
correctionGroups []correctionGroup
|
||||
groupIndexes map[string]int
|
||||
}
|
||||
|
||||
type correctionGroup struct {
|
||||
label string
|
||||
rule string
|
||||
records []string
|
||||
recordsSeen map[string]struct{}
|
||||
operatorIssues []string
|
||||
corrections diagnostics.Corrections
|
||||
}
|
||||
|
||||
func assess(value dnd.ItemOccurrenceList) validationAssessment {
|
||||
assessment := validationAssessment{groupIndexes: make(map[string]int)}
|
||||
var assessment validationAssessment
|
||||
if value.Occurrences == nil {
|
||||
assessment.operatorIssues = append(assessment.operatorIssues, "occurrences must be present")
|
||||
assessment.addCorrection("occurrences", "item-occurrence list", "Return an `occurrences` array; use an empty array when the transcript establishes no occurrences.", "")
|
||||
@@ -107,20 +99,10 @@ func assess(value dnd.ItemOccurrenceList) validationAssessment {
|
||||
}
|
||||
|
||||
func (assessment *validationAssessment) addCorrection(key, label, rule, record string) {
|
||||
index, ok := assessment.groupIndexes[key]
|
||||
if !ok {
|
||||
index = len(assessment.correctionGroups)
|
||||
assessment.groupIndexes[key] = index
|
||||
assessment.correctionGroups = append(assessment.correctionGroups, correctionGroup{label: label, rule: rule, recordsSeen: make(map[string]struct{})})
|
||||
}
|
||||
if record != "" {
|
||||
group := &assessment.correctionGroups[index]
|
||||
if _, seen := group.recordsSeen[record]; seen {
|
||||
return
|
||||
}
|
||||
group.recordsSeen[record] = struct{}{}
|
||||
group.records = append(group.records, record)
|
||||
record = "Affected " + label + " record: " + record
|
||||
}
|
||||
assessment.corrections.Add(key, rule, record)
|
||||
}
|
||||
|
||||
func (assessment validationAssessment) operatorMessage() string {
|
||||
@@ -128,16 +110,7 @@ func (assessment validationAssessment) operatorMessage() string {
|
||||
}
|
||||
|
||||
func (assessment validationAssessment) correctionGuidance() string {
|
||||
issues := make([]string, 0, len(assessment.correctionGroups)+len(assessment.operatorIssues))
|
||||
for _, group := range assessment.correctionGroups {
|
||||
issues = append(issues, group.rule)
|
||||
}
|
||||
for _, group := range assessment.correctionGroups {
|
||||
for _, record := range group.records {
|
||||
issues = append(issues, "Affected "+group.label+" record: "+record)
|
||||
}
|
||||
}
|
||||
return diagnostics.Aggregate("Correct every rejected item occurrence and return the complete replacement list", issues)
|
||||
return assessment.corrections.Guidance("Correct every rejected item occurrence and return the complete replacement list")
|
||||
}
|
||||
|
||||
func holderOperatorIssue(occurrence dnd.ItemOccurrence) string {
|
||||
@@ -159,7 +132,7 @@ func holderExpectation(kind dnd.ItemOccurrenceKind) string {
|
||||
case dnd.ItemOccurrenceKindConsumed:
|
||||
return "from present and to absent"
|
||||
case dnd.ItemOccurrenceKindTransferred:
|
||||
return "distinct named non-party holders"
|
||||
return "distinct named party-member holders"
|
||||
default:
|
||||
return "a supported holder combination"
|
||||
}
|
||||
@@ -188,19 +161,7 @@ func occurrenceContext(occurrence dnd.ItemOccurrence) string {
|
||||
if name == "" {
|
||||
context = "item with a blank contextual name"
|
||||
}
|
||||
if len(occurrence.SourceRefs) == 0 {
|
||||
context += " without a cited source range"
|
||||
} else {
|
||||
ref := occurrence.SourceRefs[0]
|
||||
if ref.StartUnitID == ref.EndUnitID {
|
||||
context += fmt.Sprintf(" at source unit %d", ref.StartUnitID)
|
||||
} else {
|
||||
context += fmt.Sprintf(" at source units %d-%d", ref.StartUnitID, ref.EndUnitID)
|
||||
}
|
||||
if len(occurrence.SourceRefs) > 1 {
|
||||
context += fmt.Sprintf(" (first of %d cited ranges)", len(occurrence.SourceRefs))
|
||||
}
|
||||
}
|
||||
context += " " + diagnostics.SourceRange(occurrence.SourceRefs)
|
||||
return context + " with from " + holderDisplay(occurrence.From) + " and to " + holderDisplay(occurrence.To)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ 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/shared"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
|
||||
itemoccurrenceshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/itemoccurrences/shape"
|
||||
)
|
||||
@@ -16,7 +17,7 @@ import (
|
||||
const (
|
||||
Key = "extract/dnd/item-occurrences/source_refs"
|
||||
ReasonCode = "invalid_item_occurrence_source_references"
|
||||
policy = "dnd.item_occurrences.source_refs.v1"
|
||||
policy = "dnd.item_occurrences.source_refs.v2"
|
||||
)
|
||||
|
||||
type Options struct{}
|
||||
@@ -42,59 +43,35 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
||||
return contracts.ValidationResult{Approved: true}, nil
|
||||
}
|
||||
index := source.NewDocumentIndex(req.Source)
|
||||
var coverage *chunkCoverage
|
||||
var coverage shared.ChunkCoverage
|
||||
if req.Stage == string(pipeline.StageExtract) {
|
||||
coverage = newChunkCoverage(req.Chunk)
|
||||
coverage = shared.NewChunkCoverage(req.Chunk)
|
||||
}
|
||||
issues := make([]string, 0)
|
||||
var corrections diagnostics.Corrections
|
||||
for occurrenceIndex, occurrence := range req.Value.Occurrences {
|
||||
for refIndex, ref := range occurrence.SourceRefs {
|
||||
record := fmt.Sprintf("Affected %s occurrence for item %s, citing %s.", diagnostics.Quote(string(occurrence.Kind)), diagnostics.Quote(occurrence.Name), diagnostics.SourceRefRange(ref))
|
||||
if err := index.ValidateRef(ref); err != nil {
|
||||
issues = append(issues, fmt.Sprintf("occurrences[%d].source_refs[%d]: %s", occurrenceIndex, refIndex, diagnostics.Truncate(err.Error())))
|
||||
corrections.Add("valid-range", "Use positive source range endpoints that occur in the supplied transcript, with the earlier unit first.", record)
|
||||
continue
|
||||
}
|
||||
if coverage != nil && !coverage.contains(req.Source, ref) {
|
||||
if req.Stage == string(pipeline.StageExtract) && !coverage.Contains(index, req.Source, ref) {
|
||||
issues = append(issues, fmt.Sprintf("occurrences[%d].source_refs[%d]: source reference is outside the current extraction chunk", occurrenceIndex, refIndex))
|
||||
corrections.Add("chunk-range", "Use only source ranges wholly contained in the supplied extraction chunk.", record)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(issues) == 0 {
|
||||
return contracts.ValidationResult{Approved: true}, nil
|
||||
}
|
||||
return contracts.ValidationResult{Approved: false, ReasonCode: ReasonCode, Message: diagnostics.Aggregate("invalid item occurrence source references", issues), CorrectionGuidance: "Return item occurrences whose source references identify valid transcript ranges within the supplied extraction chunk and directly support each occurrence."}, nil
|
||||
}
|
||||
|
||||
type chunkCoverage struct {
|
||||
sourceID string
|
||||
unitIDs map[int]struct{}
|
||||
}
|
||||
|
||||
func newChunkCoverage(chunk *source.Chunk) *chunkCoverage {
|
||||
coverage := &chunkCoverage{
|
||||
sourceID: chunk.SourceID,
|
||||
unitIDs: make(map[int]struct{}, len(chunk.Units)),
|
||||
}
|
||||
for _, unit := range chunk.Units {
|
||||
coverage.unitIDs[unit.ID] = struct{}{}
|
||||
}
|
||||
return coverage
|
||||
}
|
||||
|
||||
func (coverage *chunkCoverage) contains(doc *source.SourceDocument, ref source.SourceRef) bool {
|
||||
if coverage == nil || doc == nil || ref.SourceID != coverage.sourceID {
|
||||
return false
|
||||
}
|
||||
start, startOK := source.UnitIndex(doc, ref.StartUnitID)
|
||||
end, endOK := source.UnitIndex(doc, ref.EndUnitID)
|
||||
if !startOK || !endOK || start > end {
|
||||
return false
|
||||
}
|
||||
for position := start; position <= end; position++ {
|
||||
if _, found := coverage.unitIDs[doc.Units[position].ID]; !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
return contracts.ValidationResult{
|
||||
Approved: false,
|
||||
ReasonCode: ReasonCode,
|
||||
Message: diagnostics.Aggregate("invalid item occurrence source references", issues),
|
||||
CorrectionGuidance: corrections.Guidance("Correct every rejected item-occurrence citation and return the complete replacement occurrence list"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func Spec() pipeline.ValidatorSpec {
|
||||
|
||||
Reference in New Issue
Block a user