suite retire и проверка словаря в документе для читателя

- suite retire снимает правило, конвенцию или тему: правило остаётся заглушкой
  с датой и причиной, имя уезжает в раздел выбывших, снятие с непогашенными
  ссылками отклоняется с перечнем мест
- добавлена проверка META-30: короткое описание языка обязано называть все
  слова словаря, иначе читатель копии толкует их по памяти
- retired-раздел манифеста больше не считается объявлением пути: снятый
  префикс означает, что файл ушёл вместе с ним
This commit is contained in:
av
2026-07-27 10:51:46 +03:00
parent b29b5b5e6f
commit 5321fba89d
9 changed files with 753 additions and 7 deletions
+16 -1
View File
@@ -58,6 +58,15 @@ prefix: TIME
**ПОЧЕМУ.** Без явного смещения не видно, в какой зоне запись сделана.
`
// reading stands for the short account of the language, the one document about
// it that travels into a copy. It has to name every word of the vocabulary:
// that is the whole of META-30.
const reading = `# Как читать конвенцию
Ключевые слова: ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ.
Метки: ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО, СНЯТО.
`
// files is the content of a suite: a path from the root mapped to the text of
// the file. An empty string means "no such file": that is how a test drops a
// file the base fixture provides.
@@ -67,7 +76,7 @@ func base() files {
return files{
"suite.toml": baseManifest,
"LANGUAGE.md": "# Язык конвенций\n\nОписание языка.\n",
"READING.md": "# Как читать конвенцию\n\nКоротко.\n",
"READING.md": reading,
"conventions/time.md": baseTime,
}
}
@@ -374,6 +383,12 @@ func TestChecks(t *testing.T) {
f["conventions/rules.md"] = "---\nprefix: RULE\n---\n\n# Правила\n\n" + versionLine + "\n"
},
want: "more than one document without a topic",
}, {
name: "the short account of the language lost a word of the vocabulary",
setup: func(f files) {
f["READING.md"] = strings.Replace(reading, ", ДОПУСКАЕТСЯ", "", 1)
},
want: "does not name ДОПУСКАЕТСЯ",
}, {
name: "document without a topic carries layer keys",
setup: func(f files) {
+1 -1
View File
@@ -95,7 +95,7 @@ func layered() files {
return files{
"suite.toml": layeredManifest,
"LANGUAGE.md": "# Язык конвенций\n\nОписание языка.\n",
"READING.md": "# Как читать конвенцию\n\nКоротко.\n",
"READING.md": reading,
"conventions/arch/time.md": archTime,
"conventions/lang/go/time.md": goTime,
"conventions/arch/logging.md": archLogging,
+40
View File
@@ -1,8 +1,11 @@
package check
import (
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"git.vakhrushev.me/av/convy/internal/manifest"
"git.vakhrushev.me/av/convy/internal/suite"
@@ -15,6 +18,7 @@ func Suite(s *suite.Suite) *Report {
checkTopicNames(s, rep)
checkBaseLayers(s, rep)
checkSelfGoverning(s, rep)
checkReadingVocabulary(s, rep)
for _, d := range s.Docs {
checkForm(s, d, rep)
@@ -148,6 +152,42 @@ func checkSelfGoverning(s *suite.Suite, rep *Report) {
}
}
// checkReadingVocabulary checks that the short account of the language names
// every word of the vocabulary (META-30).
//
// The full account stays with the author of the suite, while the rules are
// applied by the reader of a copy — a person or an agent in a foreign
// repository who holds the short one and nothing else. Let the two drift apart
// and that reader starts reading the words by an older version: ДОПУСКАЕТСЯ
// turns back into an everyday "you may", a departure from ДОЛЖЕН stops
// demanding a record. Exactly what the words were introduced for fails, and it
// fails in silence.
func checkReadingVocabulary(s *suite.Suite, rep *Report) {
name := s.Manifest.Language.Reading
if name == "" {
// A suite that has not written the document yet; `suite init` says so
// among the next steps, and repeating it on every run is noise.
return
}
body, err := os.ReadFile(filepath.Join(s.Root, filepath.FromSlash(name)))
if err != nil {
// The missing file is reported by the manifest check already.
return
}
var missing []string
for _, word := range s.Vocab.Words() {
if !containsWord(string(body), word) {
missing = append(missing, word)
}
}
if len(missing) > 0 {
rep.Errorf(Form, name, 0,
"the short account of the language does not name %s: it is the only key to the text of a rule a reader of a copy holds, and a word missing from it is a word read by whatever version the reader remembers",
strings.Join(missing, ", "))
}
}
// checkBaseLayers checks that a topic holds no more than one layer without axis
// keys. The base layer is the only one of its kind: it reaches every copy, and a
// second such layer would mean two base texts in one assembled file.