2026-08-09 16:00:01 +00:00
package semanticreconcile
2026-08-09 16:57:44 +00:00
import (
"fmt"
"sort"
2026-08-28 18:25:00 +00:00
"strings"
"unicode/utf8"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
2026-08-09 16:57:44 +00:00
)
2026-08-09 16:00:01 +00:00
// ProposalResponse is the complete private structured response contract.
type ProposalResponse struct {
DuplicateGroups [ ] DuplicateGroup ` json:"duplicate_groups" `
}
2026-08-29 03:16:24 +00:00
// DuplicateGroup proposes supplied request-local candidate numbers that may
// denote one entity and identifies one supplied member as canonical.
2026-08-09 16:00:01 +00:00
type DuplicateGroup struct {
2026-08-29 03:16:24 +00:00
CandidateNumbers [ ] int ` json:"candidate_numbers" `
CanonicalCandidateNumber int ` json:"canonical_candidate_number" `
2026-08-09 16:00:01 +00:00
}
// IssueCategory identifies one stable proposal safety failure.
type IssueCategory string
const (
IssueMemberNonPositive IssueCategory = "member_non_positive"
IssueMemberUnknown IssueCategory = "member_unknown"
IssueRepeatedMember IssueCategory = "repeated_member"
IssueFewerThanTwoMembers IssueCategory = "fewer_than_two_members"
IssueCanonicalNonPositive IssueCategory = "canonical_non_positive"
IssueCanonicalUnknown IssueCategory = "canonical_unknown"
IssueCanonicalNotMember IssueCategory = "canonical_not_member"
IssueOverlappingMember IssueCategory = "overlapping_member"
)
2026-08-28 18:25:00 +00:00
var allIssueCategories = [ ] IssueCategory {
IssueMemberNonPositive ,
IssueMemberUnknown ,
IssueRepeatedMember ,
IssueFewerThanTwoMembers ,
IssueCanonicalNonPositive ,
IssueCanonicalUnknown ,
IssueCanonicalNotMember ,
IssueOverlappingMember ,
}
var issueCorrectionProse = map [ IssueCategory ] string {
2026-08-29 03:16:24 +00:00
IssueMemberNonPositive : "Use only candidate numbers in %s." ,
IssueMemberUnknown : "Remove every candidate number outside %s." ,
IssueRepeatedMember : "List each candidate number at most once within the duplicate group." ,
IssueFewerThanTwoMembers : "Include at least two distinct candidate numbers, or omit the duplicate group." ,
IssueCanonicalNonPositive : "Choose `canonical_candidate_number` from %s." ,
IssueCanonicalUnknown : "Choose `canonical_candidate_number` from %s." ,
IssueCanonicalNotMember : "Make `canonical_candidate_number` one of the `candidate_numbers` in the same duplicate group." ,
IssueOverlappingMember : "Place each candidate number in at most one duplicate group." ,
2026-08-28 18:25:00 +00:00
}
2026-08-09 16:00:01 +00:00
// Issue identifies an unsafe proposal category at its original response group
2026-08-27 18:41:59 +00:00
// index without prescribing caller diagnostic text.
2026-08-09 16:00:01 +00:00
type Issue struct {
GroupIndex int
Category IssueCategory
}
2026-08-28 18:25:00 +00:00
// IssueDetails renders stable, domain-neutral proposal diagnostics for
// operators and debug records. Its internal categories are not model guidance.
2026-08-09 16:57:44 +00:00
func IssueDetails ( issues [ ] Issue ) [ ] string {
details := make ( [ ] string , len ( issues ) )
for index , issue := range issues {
details [ index ] = fmt . Sprintf ( "group %d: %s" , issue . GroupIndex , issue . Category )
}
return details
}
2026-08-28 18:25:00 +00:00
// CorrectionDetails translates proposal issues into stable model-facing prose.
// The response-local group ordinals help the model find the defective group in
// the exact response appended to the correction request.
2026-08-29 03:16:24 +00:00
func CorrectionDetails ( issues [ ] Issue , numberRange CandidateNumberRange ) ( [ ] string , error ) {
if err := validateCorrectionRange ( numberRange ) ; err != nil {
return nil , err
}
2026-08-28 18:25:00 +00:00
groupsByCategory := make ( map [ IssueCategory ] [ ] int )
seen := make ( map [ IssueCategory ] map [ int ] struct { } )
for _ , issue := range issues {
if issue . GroupIndex < 0 {
return nil , fmt . Errorf ( "semantic reconciliation issue group index must not be negative" )
}
if _ , exists := issueCorrectionProse [ issue . Category ] ; ! exists {
return nil , fmt . Errorf ( "semantic reconciliation issue category %q has no correction guidance" , issue . Category )
}
if seen [ issue . Category ] == nil {
seen [ issue . Category ] = make ( map [ int ] struct { } )
}
if _ , exists := seen [ issue . Category ] [ issue . GroupIndex ] ; exists {
continue
}
seen [ issue . Category ] [ issue . GroupIndex ] = struct { } { }
groupsByCategory [ issue . Category ] = append ( groupsByCategory [ issue . Category ] , issue . GroupIndex )
}
details := make ( [ ] string , 0 , len ( groupsByCategory ) )
2026-08-29 03:16:24 +00:00
includeNamespaceReminder := false
2026-08-28 18:25:00 +00:00
for _ , category := range allIssueCategories {
groups := groupsByCategory [ category ]
if len ( groups ) == 0 {
continue
}
sort . Ints ( groups )
2026-08-29 03:16:24 +00:00
prose := issueCorrectionProse [ category ]
if issueNeedsCandidateRange ( category ) {
prose = fmt . Sprintf ( prose , candidateRangeDescription ( numberRange ) )
includeNamespaceReminder = true
}
details = append ( details , fmt . Sprintf ( "%s: %s" , correctionGroupLabel ( groups ) , prose ) )
}
if includeNamespaceReminder {
details = append ( details , "Candidate numbers are the request-local `candidate_number` values in that range. Transcript unit `id` values and evidence `start_unit_id` and `end_unit_id` values are source positions, not candidate numbers." )
2026-08-28 18:25:00 +00:00
}
return details , nil
}
// CorrectionGuidance builds one bounded request for a complete corrected
// proposal. Additional details let a typed owner append a domain rule without
// weakening or duplicating the shared protocol guidance.
2026-08-29 03:16:24 +00:00
func CorrectionGuidance ( issues [ ] Issue , numberRange CandidateNumberRange , additionalDetails ... string ) ( string , error ) {
details , err := CorrectionDetails ( issues , numberRange )
2026-08-28 18:25:00 +00:00
if err != nil {
return "" , err
}
for _ , detail := range additionalDetails {
if ! utf8 . ValidString ( detail ) {
return "" , fmt . Errorf ( "semantic reconciliation additional correction detail has invalid UTF-8" )
}
detail = strings . TrimSpace ( detail )
if detail == "" {
return "" , fmt . Errorf ( "semantic reconciliation additional correction detail must not be blank" )
}
details = append ( details , detail )
}
if len ( details ) == 0 {
return "" , fmt . Errorf ( "semantic reconciliation correction guidance requires at least one detail" )
}
parts := make ( [ ] string , 0 , len ( details ) + 2 )
parts = append ( parts , "The previous semantic-duplicate proposal was invalid." )
parts = append ( parts , details ... )
parts = append ( parts , "Return one complete corrected JSON response that follows the original instructions; do not return a patch or commentary." )
guidance := strings . Join ( parts , " " )
if len ( guidance ) > contracts . MaxNormalizeRetryCorrectionGuidanceBytes {
return "" , fmt . Errorf ( "semantic reconciliation correction guidance exceeds maximum length" )
}
return guidance , nil
}
2026-08-29 03:16:24 +00:00
func validateCorrectionRange ( numberRange CandidateNumberRange ) error {
if numberRange . First != 1 || numberRange . Last < 2 {
return fmt . Errorf ( "semantic reconciliation correction candidate-number range must start at 1 and include at least two candidates" )
}
return nil
}
func candidateRangeDescription ( numberRange CandidateNumberRange ) string {
return fmt . Sprintf ( "the inclusive candidate-number range %d through %d" , numberRange . First , numberRange . Last )
}
func issueNeedsCandidateRange ( category IssueCategory ) bool {
switch category {
case IssueMemberNonPositive , IssueMemberUnknown , IssueCanonicalNonPositive , IssueCanonicalUnknown :
return true
default :
return false
}
}
2026-08-28 18:25:00 +00:00
func correctionGroupLabel ( groupIndexes [ ] int ) string {
const maximumDisplayedGroups = 12
displayed := groupIndexes
if len ( displayed ) > maximumDisplayedGroups {
displayed = displayed [ : maximumDisplayedGroups ]
}
ordinals := make ( [ ] string , len ( displayed ) )
for index , groupIndex := range displayed {
ordinals [ index ] = fmt . Sprintf ( "%d" , groupIndex + 1 )
}
if len ( groupIndexes ) == 1 {
return "Duplicate group " + ordinals [ 0 ]
}
label := "Duplicate groups " + strings . Join ( ordinals , ", " )
if omitted := len ( groupIndexes ) - len ( displayed ) ; omitted > 0 {
label += fmt . Sprintf ( ", and %d additional affected group(s)" , omitted )
}
return label
}
2026-08-09 16:00:01 +00:00
// PlanGroup identifies one validated group using original candidate positions.
type PlanGroup struct {
memberPositions [ ] int
canonicalPosition int
}
// MemberPositions returns an owned, ascending list of original candidate
// positions.
func ( group PlanGroup ) MemberPositions ( ) [ ] int {
return append ( [ ] int ( nil ) , group . memberPositions ... )
}
// CanonicalPosition returns the original position of the selected canonical
// candidate.
func ( group PlanGroup ) CanonicalPosition ( ) int {
return group . canonicalPosition
}
// Plan contains deterministic, non-overlapping reconciliation groups.
type Plan struct {
groups [ ] PlanGroup
}
// Groups returns a deeply owned copy ordered by each group's earliest member.
func ( plan Plan ) Groups ( ) [ ] PlanGroup {
groups := make ( [ ] PlanGroup , len ( plan . groups ) )
for index , group := range plan . groups {
groups [ index ] = clonePlanGroup ( group )
}
return groups
}
// Assessment contains the safe plan and stable diagnostics for discarded
// response groups.
type Assessment struct {
plan Plan
discardedGroupCount int
issues [ ] Issue
}
// Plan returns an independently owned reconciliation plan.
func ( assessment Assessment ) Plan ( ) Plan {
groups := assessment . plan . Groups ( )
return Plan { groups : groups }
}
// Issues returns an owned copy ordered by original response group index.
func ( assessment Assessment ) Issues ( ) [ ] Issue {
return append ( [ ] Issue ( nil ) , assessment . issues ... )
}
// DiscardedGroupCount returns the number of response groups excluded from the
// safe plan.
func ( assessment Assessment ) DiscardedGroupCount ( ) int {
return assessment . discardedGroupCount
}
// RetryRequired reports whether any response group was discarded.
func ( assessment Assessment ) RetryRequired ( ) bool {
return assessment . discardedGroupCount > 0
}
type assessedGroup struct {
memberPositions [ ] int
canonicalPosition int
issues [ ] IssueCategory
locallyValid bool
conflicting bool
}
2026-08-29 03:16:24 +00:00
// Assess resolves request-local candidate numbers through the retained
// preparation mapping and returns only deterministic, non-overlapping groups.
2026-08-09 16:00:01 +00:00
func ( preparation Preparation ) Assess ( response ProposalResponse ) Assessment {
2026-08-29 03:16:24 +00:00
positionsByNumber := make ( map [ int ] int , len ( preparation . mappings ) )
2026-08-09 16:00:01 +00:00
for _ , mapping := range preparation . mappings {
2026-08-29 03:16:24 +00:00
positionsByNumber [ mapping . CandidateNumber ] = mapping . CandidatePosition
2026-08-09 16:00:01 +00:00
}
groups := make ( [ ] assessedGroup , len ( response . DuplicateGroups ) )
owners := make ( map [ int ] [ ] int )
for groupIndex , proposal := range response . DuplicateGroups {
2026-08-29 03:16:24 +00:00
groups [ groupIndex ] = assessGroup ( proposal , positionsByNumber )
2026-08-09 16:00:01 +00:00
if ! groups [ groupIndex ] . locallyValid {
continue
}
for _ , position := range groups [ groupIndex ] . memberPositions {
owners [ position ] = append ( owners [ position ] , groupIndex )
}
}
for _ , groupIndexes := range owners {
if len ( groupIndexes ) < 2 {
continue
}
for _ , groupIndex := range groupIndexes {
groups [ groupIndex ] . conflicting = true
}
}
assessment := Assessment { }
for groupIndex , group := range groups {
for _ , category := range group . issues {
assessment . issues = append ( assessment . issues , Issue { GroupIndex : groupIndex , Category : category } )
}
if group . conflicting {
assessment . issues = append ( assessment . issues , Issue { GroupIndex : groupIndex , Category : IssueOverlappingMember } )
}
if ! group . locallyValid || group . conflicting {
assessment . discardedGroupCount ++
continue
}
assessment . plan . groups = append ( assessment . plan . groups , PlanGroup {
memberPositions : append ( [ ] int ( nil ) , group . memberPositions ... ) ,
canonicalPosition : group . canonicalPosition ,
} )
}
sort . Slice ( assessment . plan . groups , func ( left , right int ) bool {
return assessment . plan . groups [ left ] . memberPositions [ 0 ] < assessment . plan . groups [ right ] . memberPositions [ 0 ]
} )
return assessment
}
2026-08-29 03:16:24 +00:00
func assessGroup ( proposal DuplicateGroup , positionsByNumber map [ int ] int ) assessedGroup {
2026-08-09 16:00:01 +00:00
group := assessedGroup { }
2026-08-29 03:16:24 +00:00
seenNumbers := make ( map [ int ] struct { } , len ( proposal . CandidateNumbers ) )
memberPositions := make ( map [ int ] struct { } , len ( proposal . CandidateNumbers ) )
for _ , candidateNumber := range proposal . CandidateNumbers {
if _ , repeated := seenNumbers [ candidateNumber ] ; repeated {
2026-08-09 16:00:01 +00:00
group . issues = append ( group . issues , IssueRepeatedMember )
continue
}
2026-08-29 03:16:24 +00:00
seenNumbers [ candidateNumber ] = struct { } { }
position , category := resolveMember ( candidateNumber , positionsByNumber )
2026-08-09 16:00:01 +00:00
if category != "" {
group . issues = append ( group . issues , category )
continue
}
memberPositions [ position ] = struct { } { }
group . memberPositions = append ( group . memberPositions , position )
}
if len ( memberPositions ) < 2 {
group . issues = append ( group . issues , IssueFewerThanTwoMembers )
}
2026-08-29 03:16:24 +00:00
canonicalPosition , canonicalCategory := resolveCanonical ( proposal . CanonicalCandidateNumber , positionsByNumber )
2026-08-09 16:00:01 +00:00
if canonicalCategory != "" {
group . issues = append ( group . issues , canonicalCategory )
} else {
group . canonicalPosition = canonicalPosition
if _ , member := memberPositions [ canonicalPosition ] ; ! member {
group . issues = append ( group . issues , IssueCanonicalNotMember )
}
}
sort . Ints ( group . memberPositions )
group . locallyValid = len ( group . issues ) == 0
return group
}
2026-08-29 03:16:24 +00:00
func resolveMember ( candidateNumber int , positionsByNumber map [ int ] int ) ( int , IssueCategory ) {
if candidateNumber <= 0 {
2026-08-09 16:00:01 +00:00
return 0 , IssueMemberNonPositive
}
2026-08-29 03:16:24 +00:00
position , exists := positionsByNumber [ candidateNumber ]
2026-08-09 16:00:01 +00:00
if ! exists {
return 0 , IssueMemberUnknown
}
return position , ""
}
2026-08-29 03:16:24 +00:00
func resolveCanonical ( candidateNumber int , positionsByNumber map [ int ] int ) ( int , IssueCategory ) {
if candidateNumber <= 0 {
2026-08-09 16:00:01 +00:00
return 0 , IssueCanonicalNonPositive
}
2026-08-29 03:16:24 +00:00
position , exists := positionsByNumber [ candidateNumber ]
2026-08-09 16:00:01 +00:00
if ! exists {
return 0 , IssueCanonicalUnknown
}
return position , ""
}
func clonePlanGroup ( group PlanGroup ) PlanGroup {
return PlanGroup {
memberPositions : append ( [ ] int ( nil ) , group . memberPositions ... ) ,
canonicalPosition : group . canonicalPosition ,
}
}