Derive independent glossary context and protection
This commit is contained in:
@@ -5,17 +5,17 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/schema"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/glossary"
|
||||
)
|
||||
|
||||
type protectedTermDef struct {
|
||||
identity int
|
||||
canonical string
|
||||
identities []glossary.Identity
|
||||
canonical string
|
||||
}
|
||||
|
||||
type protectedOccurrence struct {
|
||||
text string
|
||||
identity int
|
||||
identity glossary.Identity
|
||||
canonical string
|
||||
}
|
||||
|
||||
@@ -25,47 +25,13 @@ type ProtectedVocabulary struct {
|
||||
pattern *regexp.Regexp
|
||||
}
|
||||
|
||||
// ExtractProtectedTerms returns a stable, de-duplicated list of protected terms
|
||||
// derived from glossary names, aliases, synthetic plural forms, and explicit
|
||||
// plural fields.
|
||||
func ExtractProtectedTerms(glossary *schema.Glossary) []string {
|
||||
vocab := NewProtectedVocabulary(glossary)
|
||||
out := make([]string, 0, len(vocab.termsByFolded))
|
||||
for _, def := range vocab.termsByFolded {
|
||||
out = append(out, def.canonical)
|
||||
}
|
||||
sort.SliceStable(out, func(i, j int) bool {
|
||||
li := strings.ToLower(out[i])
|
||||
lj := strings.ToLower(out[j])
|
||||
if li == lj {
|
||||
return out[i] < out[j]
|
||||
}
|
||||
return li < lj
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
// NewProtectedVocabulary builds a deterministic protected vocabulary from a glossary.
|
||||
func NewProtectedVocabulary(glossary *schema.Glossary) ProtectedVocabulary {
|
||||
termsByFolded := make(map[string]protectedTermDef)
|
||||
if glossary != nil {
|
||||
for identity, entry := range glossary.Entries {
|
||||
entryTerms := append([]string{entry.Name}, entry.Aliases...)
|
||||
for _, term := range entryTerms {
|
||||
trimmed := strings.TrimSpace(term)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
addProtectedTerm(termsByFolded, trimmed, identity)
|
||||
addProtectedTerm(termsByFolded, trimmed+"s", identity)
|
||||
}
|
||||
addProtectedTerm(termsByFolded, entry.Plural, identity)
|
||||
}
|
||||
}
|
||||
|
||||
alternatives := make([]string, 0, len(termsByFolded))
|
||||
for _, def := range termsByFolded {
|
||||
alternatives = append(alternatives, regexp.QuoteMeta(def.canonical))
|
||||
// NewProtectedVocabulary prepares the independently selected terms for matching.
|
||||
func NewProtectedVocabulary(terms []glossary.Term) ProtectedVocabulary {
|
||||
termsByFolded := make(map[string]protectedTermDef, len(terms))
|
||||
alternatives := make([]string, 0, len(terms))
|
||||
for _, term := range terms {
|
||||
termsByFolded[strings.ToLower(term.Spelling)] = protectedTermDef{identities: term.Identities, canonical: term.Spelling}
|
||||
alternatives = append(alternatives, regexp.QuoteMeta(term.Spelling))
|
||||
}
|
||||
sort.SliceStable(alternatives, func(i, j int) bool { return len(alternatives[i]) > len(alternatives[j]) })
|
||||
if len(alternatives) == 0 {
|
||||
@@ -75,16 +41,13 @@ func NewProtectedVocabulary(glossary *schema.Glossary) ProtectedVocabulary {
|
||||
return ProtectedVocabulary{termsByFolded: termsByFolded, pattern: pattern}
|
||||
}
|
||||
|
||||
func addProtectedTerm(terms map[string]protectedTermDef, term string, identity int) {
|
||||
trimmed := strings.TrimSpace(term)
|
||||
if trimmed == "" {
|
||||
return
|
||||
// AllowsChange checks a proposed segment against the effective protection view.
|
||||
// Spoken-word cleanup may reduce repeated mentions while preserving each identity.
|
||||
func (v ProtectedVocabulary) AllowsChange(before, after string, allowRepetitionCleanup bool) bool {
|
||||
if v.violationReason(before, after) == "" {
|
||||
return true
|
||||
}
|
||||
folded := strings.ToLower(trimmed)
|
||||
if _, ok := terms[folded]; ok {
|
||||
return
|
||||
}
|
||||
terms[folded] = protectedTermDef{identity: identity, canonical: trimmed}
|
||||
return allowRepetitionCleanup && v.preservesRepeatedIdentities(before, after)
|
||||
}
|
||||
|
||||
func (v ProtectedVocabulary) violationReason(before, after string) string {
|
||||
@@ -99,20 +62,8 @@ func (v ProtectedVocabulary) violationReason(before, after string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (v ProtectedVocabulary) glossaryStageViolationReason(before, after string) string {
|
||||
beforeByID := v.occurrencesByIdentity(before)
|
||||
afterByID := v.occurrencesByIdentity(after)
|
||||
if reason := validateGlossaryStageIdentityPreservation(beforeByID, afterByID); reason != "" {
|
||||
return reason
|
||||
}
|
||||
if reason := validateCapitalizationTransitions(beforeByID, afterByID); reason != "" {
|
||||
return reason
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (v ProtectedVocabulary) occurrencesByIdentity(text string) map[int][]protectedOccurrence {
|
||||
byID := make(map[int][]protectedOccurrence)
|
||||
func (v ProtectedVocabulary) occurrencesByIdentity(text string) map[glossary.Identity][]protectedOccurrence {
|
||||
byID := make(map[glossary.Identity][]protectedOccurrence)
|
||||
for _, o := range v.occurrences(text) {
|
||||
byID[o.identity] = append(byID[o.identity], o)
|
||||
}
|
||||
@@ -134,16 +85,14 @@ func (v ProtectedVocabulary) occurrences(text string) []protectedOccurrence {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out = append(out, protectedOccurrence{
|
||||
text: matched,
|
||||
identity: def.identity,
|
||||
canonical: def.canonical,
|
||||
})
|
||||
for _, identity := range def.identities {
|
||||
out = append(out, protectedOccurrence{text: matched, identity: identity, canonical: def.canonical})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func validateIdentityPreservation(beforeByID, afterByID map[int][]protectedOccurrence) string {
|
||||
func validateIdentityPreservation(beforeByID, afterByID map[glossary.Identity][]protectedOccurrence) string {
|
||||
for identity, beforeItems := range beforeByID {
|
||||
if len(afterByID[identity]) < len(beforeItems) {
|
||||
return "proposal may alter protected glossary terminology"
|
||||
@@ -152,22 +101,7 @@ func validateIdentityPreservation(beforeByID, afterByID map[int][]protectedOccur
|
||||
return ""
|
||||
}
|
||||
|
||||
func validateGlossaryStageIdentityPreservation(beforeByID, afterByID map[int][]protectedOccurrence) string {
|
||||
beforeTotal := 0
|
||||
for _, items := range beforeByID {
|
||||
beforeTotal += len(items)
|
||||
}
|
||||
afterTotal := 0
|
||||
for _, items := range afterByID {
|
||||
afterTotal += len(items)
|
||||
}
|
||||
if afterTotal < beforeTotal {
|
||||
return "proposal may alter protected glossary terminology"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func validateCapitalizationTransitions(beforeByID, afterByID map[int][]protectedOccurrence) string {
|
||||
func validateCapitalizationTransitions(beforeByID, afterByID map[glossary.Identity][]protectedOccurrence) string {
|
||||
for identity, afterItems := range afterByID {
|
||||
beforeItems := beforeByID[identity]
|
||||
for i, afterItem := range afterItems {
|
||||
|
||||
Reference in New Issue
Block a user