package check_test import ( "strings" "testing" ) // A suite with two layers of one topic: a base architectural layer and a // language layer on top of it. Everything about axes, extends and the boundary // of a self-sufficient norm is checked here — a single layer cannot express it. const layeredManifest = ` [language] version = 1 description = "LANGUAGE.md" reading = "READING.md" [topics.live] time = "время" logging = "логирование" [topics.retired] [prefixes.live] TIME = "conventions/arch/time.md" GTIM = "conventions/lang/go/time.md" SLOG = "conventions/arch/logging.md" [prefixes.retired] ` const archTime = `--- topic: time prefix: TIME --- # Время Как приложение записывает моменты. ` + versionLine + ` ## Правила ### TIME-1. Момент записывается в UTC **ДОЛЖЕН.** Момент времени записывается с суффиксом Z. **ПОЧЕМУ.** Без явного смещения не видно, в какой зоне запись сделана. ` const goTime = `--- topic: time prefix: GTIM lang: go extends: arch/time.md --- # Время: реализация на Go Как требования базового слоя выполняются в Go-коде. ` + versionLine + ` ## Правила ### GTIM-1. «Сейчас» берётся у слоя хранилища **ДОЛЖЕН.** Текущее время приходит из store.Now(). **ПОЧЕМУ.** Единая точка даёт гарантированный UTC. ` const archLogging = `--- topic: logging prefix: SLOG --- # Логирование Как приложение пишет записи. ` + versionLine + ` ## Правила ### SLOG-1. Уровень выбирается по адресату **ДОЛЖЕН.** Уровень отвечает на вопрос «кому сообщение». **ПОЧЕМУ.** Адресат — единственный воспроизводимый признак. ` func layered() files { return files{ "suite.toml": layeredManifest, "LANGUAGE.md": "# Язык конвенций\n\nОписание языка.\n", "READING.md": "# Как читать конвенцию\n\nКоротко.\n", "conventions/arch/time.md": archTime, "conventions/lang/go/time.md": goTime, "conventions/arch/logging.md": archLogging, } } func TestLayeredSuiteIsClean(t *testing.T) { if got := run(t, layered()); len(got) != 0 { t.Fatalf("a sound layered suite produced findings:\n%s", messages(got)) } } func TestLayeredChecks(t *testing.T) { cases := []struct { name string setup func(files) want string }{{ name: "axis in the front matter diverges from the path", setup: func(f files) { f["conventions/lang/go/time.md"] = strings.Replace(goTime, "lang: go", "lang: python", 1) }, want: `the path puts the file on axis lang=go, while the front matter declares lang="python"`, }, { name: "extends leads into a foreign topic", setup: func(f files) { f["conventions/lang/go/time.md"] = strings.Replace(goTime, "extends: arch/time.md", "extends: arch/logging.md", 1) }, want: `with topic "logging", while the file carries topic "time"`, }, { name: "extends leads into a file that does not exist", setup: func(f files) { f["conventions/lang/go/time.md"] = strings.Replace(goTime, "extends: arch/time.md", "extends: arch/clocks.md", 1) }, want: `and the suite holds no such file`, }, { name: "topic with two layers lacking axis keys", setup: func(f files) { f["suite.toml"] = strings.Replace(layeredManifest, `GTIM = "conventions/lang/go/time.md"`, `GTIM = "conventions/second/time.md"`, 1) f["conventions/lang/go/time.md"] = "" f["conventions/second/time.md"] = strings.Replace( strings.Replace(goTime, "lang: go\n", "", 1), "extends: arch/time.md\n", "", 1) }, want: "more than one layer without axis keys", }, { name: "norm references the prefix of a foreign topic", setup: func(f files) { f["conventions/lang/go/time.md"] = strings.Replace(goTime, "**ДОЛЖЕН.** Текущее время приходит из store.Now().", "**ДОЛЖЕН.** Текущее время приходит из store.Now() и пишется по SLOG-1.", 1) }, want: `refers to SLOG-1 from the foreign topic "logging"`, }, { name: "norm references a non-base layer of its own topic", setup: func(f files) { f["conventions/arch/time.md"] = strings.Replace(archTime, "**ДОЛЖЕН.** Момент времени записывается с суффиксом Z.", "**ДОЛЖЕН.** Момент времени записывается с суффиксом Z, как требует GTIM-1.", 1) }, want: "points at a layer of this topic that is not the base one", }, { name: "rationale of the base layer references a layer above it", setup: func(f files) { f["conventions/arch/time.md"] = strings.Replace(archTime, "**ПОЧЕМУ.** Без явного смещения не видно, в какой зоне запись сделана.", "**ПОЧЕМУ.** Без явного смещения не видно зоны; в Go это выражает GTIM-1.", 1) }, want: "points at a layer of this topic that is not the base one", }, { name: "a section outside any rule references a layer above", setup: func(f files) { f["conventions/arch/time.md"] = archTime + "\n## Связано\n\nРеализация на Go — GTIM-1.\n" }, want: "points at a layer of this topic that is not the base one", }} for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { f := layered() tc.setup(f) got := messages(run(t, f)) if !strings.Contains(got, tc.want) { t.Fatalf("the check did not fire\nwanted: %s\ngot:\n%s", tc.want, got) } }) } } // TestNoFalsePositives gathers the cases where a check must stay silent. A // false positive costs more here than a miss: a check that goes red on a sound // file gets switched off altogether. func TestNoFalsePositives(t *testing.T) { cases := []struct { name string setup func(files) }{{ name: "an identifier in backticks is a sample of notation, not a reference", setup: func(f files) { f["conventions/arch/time.md"] = strings.Replace(archTime, "Без явного смещения", "На правило ссылаются идентификатором (`TIME-99`). Без явного смещения", 1) }, }, { name: "a modal word inside a fenced code block", setup: func(f files) { f["conventions/arch/time.md"] = archTime + "\n## Связано\n\n```\nДОЛЖЕН это не норма, а строка примера\n```\n" }, }, { name: "an uppercase SQL keyword in a code sample", setup: func(f files) { f["conventions/arch/time.md"] = strings.Replace(archTime, "**ПОЧЕМУ.** Без явного смещения не видно, в какой зоне запись сделана.", "**ПОЧЕМУ.** Без явного смещения не видно зоны.\n\n```sql\nSELECT 1 WHERE a AND b OR c\n```", 1) }, }, { name: "a language layer's norm references the base layer of its own topic", setup: func(f files) { f["conventions/lang/go/time.md"] = strings.Replace(goTime, "**ДОЛЖЕН.** Текущее время приходит из store.Now().", "**ДОЛЖЕН.** Текущее время приходит из store.Now() в форме TIME-1.", 1) }, }, { name: "a mention of a step in the rationale is not a second norm", setup: func(f files) { f["conventions/arch/time.md"] = strings.Replace(archTime, "**ПОЧЕМУ.** Без явного смещения не видно, в какой зоне запись сделана.", "**ПОЧЕМУ.** Для ступени СЛЕДУЕТ это было бы честно, но здесь ломается сортировка.", 1) }, }, { name: "a stub of a retired rule with a date and a reason", setup: func(f files) { f["conventions/arch/time.md"] = archTime + "\n### TIME-2. Ширина строки фиксируется\n\n**СНЯТО 2026-07-26.** Ширина следует из TIME-1 и отдельного правила не требует.\n" }, }, { name: "a scenario block in the suite's own vocabulary", setup: func(f files) { f["conventions/arch/time.md"] = archTime + "\n## Стык правил\n\nКОГДА зависимость недоступна И ретраи исчерпаны\nТОГДА запись делается один раз (TIME-1)\n" }, }, { name: "an uppercase SQL connective mid-sentence outside a fence", setup: func(f files) { f["conventions/arch/time.md"] = strings.Replace(archTime, "**ПОЧЕМУ.** Без явного смещения не видно, в какой зоне запись сделана.", "**ПОЧЕМУ.** Условие `WHERE a AND b OR c` сортировку не спасает.", 1) }, }, { name: "a rationale references a foreign topic, which META-20 allows", setup: func(f files) { f["conventions/lang/go/time.md"] = strings.Replace(goTime, "**ПОЧЕМУ.** Единая точка даёт гарантированный UTC.", "**ПОЧЕМУ.** Единая точка даёт гарантированный UTC; тот же довод стоит за SLOG-1.", 1) }, }, { name: "a section outside any rule references a foreign topic", setup: func(f files) { f["conventions/lang/go/time.md"] = goTime + "\n## Связано\n\nКонвенция logging, правило SLOG-1.\n" }, }, { name: "a layer references the base layer of its topic outside a norm", setup: func(f files) { f["conventions/lang/go/time.md"] = strings.Replace(goTime, "**ПОЧЕМУ.** Единая точка даёт гарантированный UTC.", "**ПОЧЕМУ.** Единая точка даёт гарантированный UTC, чего и требует TIME-1.", 1) }, }, { name: "the single document without a topic is the one the suite governs itself by", setup: func(f files) { f["suite.toml"] = strings.Replace(layeredManifest, `SLOG = "conventions/arch/logging.md"`, `SLOG = "conventions/arch/logging.md"`+"\nMETA = \"GUIDE.md\"", 1) f["GUIDE.md"] = "---\nprefix: META\n---\n\n# Как мы ведём конвенции\n\n" + versionLine + "\n" }, }} for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { f := layered() tc.setup(f) if got := run(t, f); len(got) != 0 { t.Fatalf("a check fired where it must not:\n%s", messages(got)) } }) } }