Clarify semantic reconciliation candidate numbers
This commit is contained in:
@@ -14,11 +14,11 @@ type ProposalResponse struct {
|
||||
DuplicateGroups []DuplicateGroup `json:"duplicate_groups"`
|
||||
}
|
||||
|
||||
// DuplicateGroup proposes supplied request-local candidate IDs that may denote
|
||||
// one entity and identifies one supplied member as canonical.
|
||||
// DuplicateGroup proposes supplied request-local candidate numbers that may
|
||||
// denote one entity and identifies one supplied member as canonical.
|
||||
type DuplicateGroup struct {
|
||||
CandidateIDs []int `json:"candidate_ids"`
|
||||
CanonicalCandidateID int `json:"canonical_candidate_id"`
|
||||
CandidateNumbers []int `json:"candidate_numbers"`
|
||||
CanonicalCandidateNumber int `json:"canonical_candidate_number"`
|
||||
}
|
||||
|
||||
// IssueCategory identifies one stable proposal safety failure.
|
||||
@@ -47,14 +47,14 @@ var allIssueCategories = []IssueCategory{
|
||||
}
|
||||
|
||||
var issueCorrectionProse = map[IssueCategory]string{
|
||||
IssueMemberNonPositive: "Use only positive candidate IDs from the supplied candidate list.",
|
||||
IssueMemberUnknown: "Remove every candidate ID that is not present in the supplied candidate list.",
|
||||
IssueRepeatedMember: "List each candidate ID at most once within the duplicate group.",
|
||||
IssueFewerThanTwoMembers: "Include at least two distinct candidate IDs, or omit the duplicate group.",
|
||||
IssueCanonicalNonPositive: "Choose a positive canonical_candidate_id from the supplied candidate list.",
|
||||
IssueCanonicalUnknown: "Choose canonical_candidate_id from the supplied candidate list.",
|
||||
IssueCanonicalNotMember: "Make canonical_candidate_id one of the candidate_ids in the same duplicate group.",
|
||||
IssueOverlappingMember: "Place each candidate ID in at most one duplicate group.",
|
||||
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.",
|
||||
}
|
||||
|
||||
// Issue identifies an unsafe proposal category at its original response group
|
||||
@@ -77,7 +77,10 @@ func IssueDetails(issues []Issue) []string {
|
||||
// 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.
|
||||
func CorrectionDetails(issues []Issue) ([]string, error) {
|
||||
func CorrectionDetails(issues []Issue, numberRange CandidateNumberRange) ([]string, error) {
|
||||
if err := validateCorrectionRange(numberRange); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
groupsByCategory := make(map[IssueCategory][]int)
|
||||
seen := make(map[IssueCategory]map[int]struct{})
|
||||
for _, issue := range issues {
|
||||
@@ -98,13 +101,22 @@ func CorrectionDetails(issues []Issue) ([]string, error) {
|
||||
}
|
||||
|
||||
details := make([]string, 0, len(groupsByCategory))
|
||||
includeNamespaceReminder := false
|
||||
for _, category := range allIssueCategories {
|
||||
groups := groupsByCategory[category]
|
||||
if len(groups) == 0 {
|
||||
continue
|
||||
}
|
||||
sort.Ints(groups)
|
||||
details = append(details, fmt.Sprintf("%s: %s", correctionGroupLabel(groups), issueCorrectionProse[category]))
|
||||
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.")
|
||||
}
|
||||
return details, nil
|
||||
}
|
||||
@@ -112,8 +124,8 @@ func CorrectionDetails(issues []Issue) ([]string, error) {
|
||||
// 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.
|
||||
func CorrectionGuidance(issues []Issue, additionalDetails ...string) (string, error) {
|
||||
details, err := CorrectionDetails(issues)
|
||||
func CorrectionGuidance(issues []Issue, numberRange CandidateNumberRange, additionalDetails ...string) (string, error) {
|
||||
details, err := CorrectionDetails(issues, numberRange)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -142,6 +154,26 @@ func CorrectionGuidance(issues []Issue, additionalDetails ...string) (string, er
|
||||
return guidance, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func correctionGroupLabel(groupIndexes []int) string {
|
||||
const maximumDisplayedGroups = 12
|
||||
displayed := groupIndexes
|
||||
@@ -232,18 +264,18 @@ type assessedGroup struct {
|
||||
conflicting bool
|
||||
}
|
||||
|
||||
// Assess resolves request-local IDs through the retained preparation mapping
|
||||
// and returns only deterministic, non-overlapping groups.
|
||||
// Assess resolves request-local candidate numbers through the retained
|
||||
// preparation mapping and returns only deterministic, non-overlapping groups.
|
||||
func (preparation Preparation) Assess(response ProposalResponse) Assessment {
|
||||
positionsByID := make(map[int]int, len(preparation.mappings))
|
||||
positionsByNumber := make(map[int]int, len(preparation.mappings))
|
||||
for _, mapping := range preparation.mappings {
|
||||
positionsByID[mapping.CandidateID] = mapping.CandidatePosition
|
||||
positionsByNumber[mapping.CandidateNumber] = mapping.CandidatePosition
|
||||
}
|
||||
|
||||
groups := make([]assessedGroup, len(response.DuplicateGroups))
|
||||
owners := make(map[int][]int)
|
||||
for groupIndex, proposal := range response.DuplicateGroups {
|
||||
groups[groupIndex] = assessGroup(proposal, positionsByID)
|
||||
groups[groupIndex] = assessGroup(proposal, positionsByNumber)
|
||||
if !groups[groupIndex].locallyValid {
|
||||
continue
|
||||
}
|
||||
@@ -283,17 +315,17 @@ func (preparation Preparation) Assess(response ProposalResponse) Assessment {
|
||||
return assessment
|
||||
}
|
||||
|
||||
func assessGroup(proposal DuplicateGroup, positionsByID map[int]int) assessedGroup {
|
||||
func assessGroup(proposal DuplicateGroup, positionsByNumber map[int]int) assessedGroup {
|
||||
group := assessedGroup{}
|
||||
seenIDs := make(map[int]struct{}, len(proposal.CandidateIDs))
|
||||
memberPositions := make(map[int]struct{}, len(proposal.CandidateIDs))
|
||||
for _, candidateID := range proposal.CandidateIDs {
|
||||
if _, repeated := seenIDs[candidateID]; repeated {
|
||||
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 {
|
||||
group.issues = append(group.issues, IssueRepeatedMember)
|
||||
continue
|
||||
}
|
||||
seenIDs[candidateID] = struct{}{}
|
||||
position, category := resolveMember(candidateID, positionsByID)
|
||||
seenNumbers[candidateNumber] = struct{}{}
|
||||
position, category := resolveMember(candidateNumber, positionsByNumber)
|
||||
if category != "" {
|
||||
group.issues = append(group.issues, category)
|
||||
continue
|
||||
@@ -305,7 +337,7 @@ func assessGroup(proposal DuplicateGroup, positionsByID map[int]int) assessedGro
|
||||
group.issues = append(group.issues, IssueFewerThanTwoMembers)
|
||||
}
|
||||
|
||||
canonicalPosition, canonicalCategory := resolveCanonical(proposal.CanonicalCandidateID, positionsByID)
|
||||
canonicalPosition, canonicalCategory := resolveCanonical(proposal.CanonicalCandidateNumber, positionsByNumber)
|
||||
if canonicalCategory != "" {
|
||||
group.issues = append(group.issues, canonicalCategory)
|
||||
} else {
|
||||
@@ -320,22 +352,22 @@ func assessGroup(proposal DuplicateGroup, positionsByID map[int]int) assessedGro
|
||||
return group
|
||||
}
|
||||
|
||||
func resolveMember(candidateID int, positionsByID map[int]int) (int, IssueCategory) {
|
||||
if candidateID <= 0 {
|
||||
func resolveMember(candidateNumber int, positionsByNumber map[int]int) (int, IssueCategory) {
|
||||
if candidateNumber <= 0 {
|
||||
return 0, IssueMemberNonPositive
|
||||
}
|
||||
position, exists := positionsByID[candidateID]
|
||||
position, exists := positionsByNumber[candidateNumber]
|
||||
if !exists {
|
||||
return 0, IssueMemberUnknown
|
||||
}
|
||||
return position, ""
|
||||
}
|
||||
|
||||
func resolveCanonical(candidateID int, positionsByID map[int]int) (int, IssueCategory) {
|
||||
if candidateID <= 0 {
|
||||
func resolveCanonical(candidateNumber int, positionsByNumber map[int]int) (int, IssueCategory) {
|
||||
if candidateNumber <= 0 {
|
||||
return 0, IssueCanonicalNonPositive
|
||||
}
|
||||
position, exists := positionsByID[candidateID]
|
||||
position, exists := positionsByNumber[candidateNumber]
|
||||
if !exists {
|
||||
return 0, IssueCanonicalUnknown
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user