package evidencecontext import ( "fmt" "sort" "gitea.maximumdirect.net/eric/notarius/internal/core/source" ) type expandedRange struct { startPos int endPos int } // Build validates projected source references, expands them by source-document // position, and returns their ordered union as an owned source-unit excerpt. func Build(request BuildRequest) (Document, error) { if request.WindowUnits < 0 { return nil, fmt.Errorf("window_units must not be negative") } if err := source.ValidateDocument(request.Source); err != nil { return nil, fmt.Errorf("validate source document: %w", err) } digest, err := source.DigestDocument(request.Source) if err != nil { return nil, fmt.Errorf("digest source document: %w", err) } if digest != request.Source.Digest { return nil, fmt.Errorf("source digest does not match source document digest") } index := source.NewDocumentIndex(request.Source) ranges := make([]expandedRange, 0, len(request.SourceRefs)) for refIndex, ref := range request.SourceRefs { if err := index.ValidateRef(ref); err != nil { return nil, fmt.Errorf("source reference[%d]: %w", refIndex, err) } startPos, _ := index.Position(ref.StartUnitID) endPos, _ := index.Position(ref.EndUnitID) ranges = append(ranges, expandedRange{ startPos: expandStart(startPos, request.WindowUnits), endPos: expandEnd(endPos, len(request.Source.Units), request.WindowUnits), }) } merged := mergeRanges(ranges) document := make(Document, 0) for _, value := range merged { for position := value.startPos; position <= value.endPos; position++ { unit, err := cloneSourceUnit(request.Source.Units[position]) if err != nil { return nil, fmt.Errorf("clone source unit at position %d: %w", position, err) } document = append(document, unit) } } return document, nil } func expandStart(position, window int) int { if window > position { return 0 } return position - window } func expandEnd(position, length, window int) int { last := length - 1 if window > last-position { return last } return position + window } func mergeRanges(values []expandedRange) []expandedRange { if len(values) == 0 { return nil } sort.Slice(values, func(i, j int) bool { if values[i].startPos != values[j].startPos { return values[i].startPos < values[j].startPos } return values[i].endPos < values[j].endPos }) merged := make([]expandedRange, 0, len(values)) for _, value := range values { if len(merged) == 0 || value.startPos > merged[len(merged)-1].endPos+1 { merged = append(merged, value) continue } if value.endPos > merged[len(merged)-1].endPos { merged[len(merged)-1].endPos = value.endPos } } return merged }