// Package lang держит словари языка конвенций: слова, которыми записаны // модальность правила, метки его блоков и связки сценарного блока. // // Словарь — свойство версии языка и естественного языка набора, а не самого // набора: версия 1 по-русски задаёт один и тот же список слов в любом // репозитории, и повторять его в каждом манифесте незачем. Пока спецификация // языка живёт вместе с каноном, словари лежат здесь; когда она уедет в // отдельный репозиторий со своими файлами словарей, источником станут они, а // форма Vocabulary и все проверки поверх неё останутся прежними. package lang import ( "fmt" "sort" "strings" "unicode" "unicode/utf8" ) // Level — ступень шкалы обязательности. Ступеней пять в четырёх категориях // ISO/IEC Directives, Part 2; какими словами они названы — параметр // естественного языка, а сама шкала одна на все словари. type Level int const ( Requirement Level = iota + 1 Prohibition Recommendation RecommendationAgainst Permission ) // String даёт имя ступени для сообщений об ошибках — не слово словаря, а роль. func (l Level) String() string { switch l { case Requirement: return "требование" case Prohibition: return "запрет" case Recommendation: return "рекомендация" case RecommendationAgainst: return "рекомендация против" case Permission: return "разрешение" } return "неизвестная ступень" } // Mark — метка блока правила. Метки обязательности не задают, а размечают: // что здесь обоснование, что иллюстрация, что запись о механизации, что // заглушка на месте снятого правила. type Mark int const ( Rationale Mark = iota + 1 Examples Mechanized Retired ) func (m Mark) String() string { switch m { case Rationale: return "обоснование" case Examples: return "примеры" case Mechanized: return "механизация" case Retired: return "снятое правило" } return "неизвестная метка" } // Connective — служебное слово сценарного блока. В строку о версии языка эти // слова не входят и под проверку «модальные слова вне правил» не подпадают: // обязательности они не задают, только структуру. type Connective int const ( When Connective = iota + 1 Then And Or ) // Vocabulary — словарь одной версии языка на одном естественном языке. type Vocabulary struct { Version int Code string Modals map[string]Level Marks map[string]Mark Scenario map[string]Connective } // Modal сообщает ступень слова, если слово принадлежит шкале этого словаря. func (v Vocabulary) Modal(word string) (Level, bool) { l, ok := v.Modals[word] return l, ok } // Mark сообщает роль метки, если слово принадлежит меткам этого словаря. func (v Vocabulary) Mark(word string) (Mark, bool) { m, ok := v.Marks[word] return m, ok } // Word возвращает слово, которым в этом словаре записана ступень. func (v Vocabulary) Word(l Level) string { for w, got := range v.Modals { if got == l { return w } } return "" } // MarkWord возвращает слово, которым в этом словаре записана метка. func (v Vocabulary) MarkWord(m Mark) string { for w, got := range v.Marks { if got == m { return w } } return "" } // Lead возвращает слово словаря, которым начинается text, и его длину. // Длиннейшее совпадение выигрывает: «НЕ ДОЛЖЕН» не должен читаться как // «ДОЛЖЕН», а метка с датой («СНЯТО 2026-07-26») — как метка без неё. func (v Vocabulary) Lead(text string) (string, bool) { best := "" for _, w := range v.Words() { if len(w) <= len(best) { continue } if !hasWordPrefix(text, w) { continue } best = w } return best, best != "" } // Words перечисляет все слова словаря, которые язык объявляет в строке о // версии: ступени шкалы и метки. Связки сценария сюда не входят. func (v Vocabulary) Words() []string { words := make([]string, 0, len(v.Modals)+len(v.Marks)) for w := range v.Modals { words = append(words, w) } for w := range v.Marks { words = append(words, w) } sort.Strings(words) return words } // hasWordPrefix проверяет, что text начинается со слова w и слово на этом // кончается: «ДОЛЖЕНСТВОВАНИЕ» словом ДОЛЖЕН не является. func hasWordPrefix(text, w string) bool { if !strings.HasPrefix(text, w) { return false } rest := text[len(w):] if rest == "" { return true } r, _ := utf8.DecodeRuneInString(rest) return !unicode.IsLetter(r) && !unicode.IsDigit(r) } // registry — словари, известные бинарю. Ключ верхнего уровня — версия языка, // вложенный — код естественного языка набора. var registry = map[int]map[string]Vocabulary{ 1: { "ru": { Version: 1, Code: "ru", Modals: map[string]Level{ "ДОЛЖЕН": Requirement, "НЕ ДОЛЖЕН": Prohibition, "СЛЕДУЕТ": Recommendation, "НЕ СЛЕДУЕТ": RecommendationAgainst, "ДОПУСКАЕТСЯ": Permission, }, Marks: map[string]Mark{ "ПОЧЕМУ": Rationale, "ПРИМЕРЫ": Examples, "МЕХАНИЗИРОВАНО": Mechanized, "СНЯТО": Retired, }, Scenario: map[string]Connective{ "КОГДА": When, "ТОГДА": Then, "И": And, "ИЛИ": Or, }, }, "en": { Version: 1, Code: "en", Modals: map[string]Level{ "MUST": Requirement, "MUST NOT": Prohibition, "SHOULD": Recommendation, "SHOULD NOT": RecommendationAgainst, "MAY": Permission, }, Marks: map[string]Mark{ "WHY": Rationale, "EXAMPLES": Examples, "MECHANIZED": Mechanized, "RETIRED": Retired, }, Scenario: map[string]Connective{ "WHEN": When, "THEN": Then, "AND": And, "OR": Or, }, }, }, } // Lookup выдаёт словарь версии языка на указанном естественном языке. func Lookup(version int, code string) (Vocabulary, error) { byCode, ok := registry[version] if !ok { return Vocabulary{}, fmt.Errorf("версия языка %d инструменту неизвестна, известны: %s", version, versions()) } v, ok := byCode[code] if !ok { return Vocabulary{}, fmt.Errorf("словарь %q для версии языка %d инструменту неизвестен, известны: %s", code, version, codes(version)) } return v, nil } // Foreign перечисляет слова чужих словарей той же версии — те, по которым // видно смесь словарей. Слова, совпадающие с собственными, отброшены. func Foreign(version int, code string) map[string]string { own, err := Lookup(version, code) if err != nil { return nil } mine := make(map[string]bool) for _, w := range own.Words() { mine[w] = true } foreign := make(map[string]string) for otherCode, other := range registry[version] { if otherCode == code { continue } for _, w := range other.Words() { if !mine[w] { foreign[w] = otherCode } } } return foreign } func versions() string { out := make([]string, 0, len(registry)) for v := range registry { out = append(out, fmt.Sprint(v)) } sort.Strings(out) return strings.Join(out, ", ") } func codes(version int) string { out := make([]string, 0, len(registry[version])) for c := range registry[version] { out = append(out, c) } sort.Strings(out) return strings.Join(out, ", ") }