feat: add EntryWarnings and VocabularyBuilder

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 15:11:48 +02:00
co-authored by Claude Sonnet 5.5
parent 93bf59e090
commit fe24292279
4 changed files with 165 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
import Foundation
public struct EntryWarning: Equatable, Sendable {
public enum Level: Int, Comparable, Sendable {
case note = 0, warning, error
public static func < (a: Level, b: Level) -> Bool { a.rawValue < b.rawValue }
}
public let level: Level
public let message: String
public init(level: Level, message: String) {
self.level = level
self.message = message
}
}
/// Flags dictionary entries that are likely to corrupt normal text.
public enum EntryWarnings {
/// - Parameter others: all entries except the one being checked.
public static func check(_ kind: DictionaryEntry.Kind, among others: [DictionaryEntry], knowledge: WordKnowledge) -> [EntryWarning] {
let source: String
let isCorrection: Bool
switch kind {
case .term(let term): (source, isCorrection) = (term, false)
case .correction(let heard, _): (source, isCorrection) = (heard, true)
}
let parts = EntryKey.parts(source)
guard !parts.isEmpty else { return [EntryWarning(level: .error, message: "Leeg: vul een woord in.")] }
var result: [EntryWarning] = []
if others.contains(where: { EntryKey.key($0.source) == EntryKey.key(source) }) {
result.append(EntryWarning(level: .error, message: "Dubbele invoer: deze bron staat er al, de eerste telt."))
}
if !isCorrection {
if parts.count == 1, knowledge.isWord(source) {
result.append(EntryWarning(level: .note, message: "Gewoon woord: krijgt alleen voorrang bij het model, geen automatische hoofdletterregel."))
}
return result.sorted { $0.level > $1.level }
}
if parts.count == 1, knowledge.isWord(parts[0]) {
result.append(EntryWarning(level: .warning, message: "'\(parts[0])' is een gewoon woord en wordt overal in je tekst vervangen."))
}
if source.filter(\.isLetter).count < 3 {
result.append(EntryWarning(level: .warning, message: "Erg kort (minder dan 3 letters): veel kans op ongewenste vervangingen."))
}
if parts.count > 1 {
var flagged = false
if let common = parts.first(where: { knowledge.isFunctionWord($0) }) {
result.append(EntryWarning(level: .warning, message: "Bevat het veelvoorkomende woord '\(common)'; dit kan gewone zinnen raken."))
flagged = true
}
let glued = parts.joined()
if knowledge.isWord(glued) {
result.append(EntryWarning(level: .warning, message: "Aaneengeschreven ('\(glued)') is een bestaand woord en wordt ook vervangen."))
flagged = true
}
if !flagged, parts.allSatisfy({ knowledge.isWord($0) }) {
result.append(EntryWarning(level: .note, message: "Alle delen zijn gewone woorden: alleen de hele combinatie wordt vervangen."))
}
}
return result.sorted { $0.level > $1.level }
}
}
+19
View File
@@ -0,0 +1,19 @@
import Foundation
/// The short word list handed to the speech engine as context.
public enum VocabularyBuilder {
/// Terms first (file order), then the unique right-hand sides of correction pairs; at most `maxTerms` words.
public static func build(from entries: [DictionaryEntry], maxTerms: Int = 24) -> [String] {
var out: [String] = []
var seen = Set<String>()
func add(_ word: String) {
let key = word.lowercased()
guard !word.isEmpty, !seen.contains(key) else { return }
seen.insert(key)
out.append(word)
}
for entry in entries { if case .term(let term) = entry.kind { add(term) } }
for entry in entries { if case .correction(_, let wrote) = entry.kind { add(wrote) } }
return Array(out.prefix(maxTerms))
}
}