проектные команды и ссылки на источник

- заведён internal/source: уровни ссылаются друг на друга путём на диске
  или git-репозиторием, ревизия закрепляется хвостом #ref; клон делается
  заново и удаляется, кэша нет
- добавлены init, add, pull, list, check в проекте — манифест
  .conventions.toml, сборка копий по разу на компонент, маркер локальной
  части, READING.md рядом
- проверки формы развязаны с набором: принимают lang.Vocabulary, а язык
  копии узнаётся по строке о версии — манифеста рядом с ней нет
This commit is contained in:
av
2026-07-27 20:42:18 +03:00
parent 4615de6e86
commit 23d88c4048
27 changed files with 3009 additions and 57 deletions
+75
View File
@@ -292,6 +292,81 @@ func Lookup(version int, code string) (Vocabulary, error) {
return v, nil
}
// Recognize picks the vocabulary a text is written in by the words it names.
//
// A copy in a consuming repository declares its language by the version line
// and by nothing else: no manifest travels with it, and no path back to the
// suite is written anywhere in it. So a check run outside a suite learns which
// words are normative the same way a reader does — off the line the document
// carries. The version number decides between vocabularies naming the same
// words, which is what two versions of one natural language would do.
func Recognize(text string) (Vocabulary, bool) {
var best Vocabulary
for _, version := range sortedVersions() {
for _, code := range sortedCodes(version) {
v := registry[version][code]
if !namesAll(text, v.Words()) {
continue
}
if best.Version == 0 || matchesVersion(text, v.Version) {
best = v
}
}
}
return best, best.Version != 0
}
func namesAll(text string, words []string) bool {
for _, w := range words {
if !strings.Contains(text, w) {
return false
}
}
return true
}
// matchesVersion looks for the number as a whole word, so that version 1 is not
// read out of "version 12".
func matchesVersion(text string, version int) bool {
number := fmt.Sprint(version)
for i := 0; ; {
j := strings.Index(text[i:], number)
if j < 0 {
return false
}
start, end := i+j, i+j+len(number)
before := start == 0 || !isDigit(rune(text[start-1]))
after := end == len(text) || !isDigit(rune(text[end]))
if before && after {
return true
}
i = end
if i >= len(text) {
return false
}
}
}
func isDigit(r rune) bool { return r >= '0' && r <= '9' }
func sortedVersions() []int {
out := make([]int, 0, len(registry))
for v := range registry {
out = append(out, v)
}
sort.Ints(out)
return out
}
func sortedCodes(version int) []string {
out := make([]string, 0, len(registry[version]))
for c := range registry[version] {
out = append(out, c)
}
sort.Strings(out)
return out
}
// Foreign lists the words of the other vocabularies of the same version — the
// ones that give away a mixture of vocabularies. Words that coincide with the
// suite's own are dropped.