манифест набора переименован в .conventions-suite.toml

- оба манифеста стали данными, которые инструмент переписывает целиком,
  так что и наборный получает точку в начале: он служебный файл, а не
  документ репозитория
- переименование механическое, одна константа manifest.Name; литералы в
  фикстурах и ссылки в README.md и CLAUDE.md обновлены следом
This commit is contained in:
av
2026-07-28 09:55:26 +03:00
parent 92bd1f463d
commit a597805f3c
9 changed files with 38 additions and 35 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ CLI для управления конвенциями. Модель здесь
```
internal/lang словарь: реестр «версия языка × естественный язык»
internal/source ссылки между уровнями: путь на диске, git-репозиторий
internal/manifest suite.toml и .conventions.toml — чтение и запись
internal/manifest оба манифеста: чтение и запись
internal/doc разбор документа: шапка, области правил, блоки
internal/suite сборка набора в память, отбор слоёв под компонент
internal/project сборка копий в проекте: разделы, маркер, READING.md
+7 -6
View File
@@ -13,7 +13,7 @@ CLI для управления конвенциями разработки: в
| Уровень | Что это |
|---|---|
| язык | как записывается правило: слова, версия, `READING.md` |
| набор | репозиторий с конвенциями, манифестом `suite.toml` и обвязкой |
| набор | репозиторий с конвенциями, манифестом `.conventions-suite.toml` и обвязкой |
| проект | репозиторий-потребитель с манифестом `.conventions.toml` |
| тема | набор правил об одном фокусе разработки; единица подписки |
| слой | один файл темы: базовый, языковой или стековый |
@@ -90,7 +90,7 @@ go build -o convy .
convy check проверить форму того, что здесь
```
Контекст определяется по манифесту рядом: `suite.toml` — набор,
Контекст определяется по манифесту рядом: `.conventions-suite.toml` — набор,
`.conventions.toml` — проект. Наугад не делается ничего: команда не того уровня
отказывает и подсказывает нужную.
@@ -194,9 +194,9 @@ topics = ["logging", "time"]
```
$ convy suite add --topic time --about "время" --prefix TIME --title "Время"
suite.toml holds 1 key the tool does not know (language.descriptoin); a write
goes out of what the tool understands, so the key would be dropped — fix the
spelling first
.conventions-suite.toml holds 1 key the tool does not know
(language.descriptoin); a write goes out of what the tool understands, so the
key would be dropped — fix the spelling first
```
## Манифест — источник истины
@@ -278,7 +278,8 @@ errors: 1, warnings: 0
Слова, которыми записаны модальность и метки, — свойство версии языка и
естественного языка набора, а не самого набора. Инструмент знает их сам, и
`suite.toml` их не дублирует: достаточно `[language] version` и `lang`.
`.conventions-suite.toml` их не дублирует: хватает `[language] version` и
`lang`.
Поэтому ступень называется категорией, а не словом:
+14 -14
View File
@@ -74,10 +74,10 @@ type files map[string]string
func base() files {
return files{
"suite.toml": baseManifest,
"LANGUAGE.md": "# Язык конвенций\n\nОписание языка.\n",
"READING.md": reading,
"conventions/time.md": baseTime,
".conventions-suite.toml": baseManifest,
"LANGUAGE.md": "# Язык конвенций\n\nОписание языка.\n",
"READING.md": reading,
"conventions/time.md": baseTime,
}
}
@@ -270,14 +270,14 @@ func TestChecks(t *testing.T) {
}, {
name: "topic listed among the retired ones",
setup: func(f files) {
f["suite.toml"] = strings.Replace(baseManifest,
f[".conventions-suite.toml"] = strings.Replace(baseManifest,
"[topics.retired]", `[topics.retired]`+"\ntime = \"снята 2026-07-01\"", 1)
},
want: "is listed both live and retired",
}, {
name: "live topic without layers",
setup: func(f files) {
f["suite.toml"] = strings.Replace(baseManifest,
f[".conventions-suite.toml"] = strings.Replace(baseManifest,
`time = "время: хранение, зоны, форматы"`,
`time = "время"`+"\nlogging = \"логирование\"", 1)
},
@@ -285,7 +285,7 @@ func TestChecks(t *testing.T) {
}, {
name: "declared file is missing",
setup: func(f files) {
f["suite.toml"] = strings.Replace(baseManifest,
f[".conventions-suite.toml"] = strings.Replace(baseManifest,
`TIME = "conventions/time.md"`,
`TIME = "conventions/time.md"`+"\nSLOG = \"conventions/logging.md\"", 1)
},
@@ -299,7 +299,7 @@ func TestChecks(t *testing.T) {
}, {
name: "prefix starts with X",
setup: func(f files) {
f["suite.toml"] = strings.Replace(baseManifest,
f[".conventions-suite.toml"] = strings.Replace(baseManifest,
`TIME = "conventions/time.md"`, `XTIM = "conventions/time.md"`, 1)
f["conventions/time.md"] = strings.ReplaceAll(baseTime, "TIME", "XTIM")
},
@@ -307,7 +307,7 @@ func TestChecks(t *testing.T) {
}, {
name: "one file with two prefixes declared",
setup: func(f files) {
f["suite.toml"] = strings.Replace(baseManifest,
f[".conventions-suite.toml"] = strings.Replace(baseManifest,
`TIME = "conventions/time.md"`,
`TIME = "conventions/time.md"`+"\nGTIM = \"conventions/time.md\"", 1)
},
@@ -315,7 +315,7 @@ func TestChecks(t *testing.T) {
}, {
name: "unknown manifest key",
setup: func(f files) {
f["suite.toml"] = baseManifest + "\n[extra]\nkey = 1\n"
f[".conventions-suite.toml"] = baseManifest + "\n[extra]\nkey = 1\n"
},
want: "is unknown to the tool",
}, {
@@ -362,21 +362,21 @@ func TestChecks(t *testing.T) {
setup: func(f files) {
// A bare non-ASCII key TOML rejects on its own; a quoted one
// passes straight through, which is what the check is for.
f["suite.toml"] = strings.Replace(baseManifest, "time =", `"время" =`, 1)
f[".conventions-suite.toml"] = strings.Replace(baseManifest, "time =", `"время" =`, 1)
f["conventions/time.md"] = strings.Replace(baseTime, "topic: time", "topic: время", 1)
},
want: "is not usable as a file name",
}, {
name: "topic name is not lower kebab-case",
setup: func(f files) {
f["suite.toml"] = strings.Replace(baseManifest, "time =", "Time_Zone =", 1)
f[".conventions-suite.toml"] = strings.Replace(baseManifest, "time =", "Time_Zone =", 1)
f["conventions/time.md"] = strings.Replace(baseTime, "topic: time", "topic: Time_Zone", 1)
},
want: "is not lower kebab-case",
}, {
name: "a second document without a topic",
setup: func(f files) {
f["suite.toml"] = strings.Replace(baseManifest,
f[".conventions-suite.toml"] = strings.Replace(baseManifest,
`TIME = "conventions/time.md"`,
`TIME = "conventions/time.md"`+"\nMETA = \"GUIDE.md\"\nRULE = \"conventions/rules.md\"", 1)
f["GUIDE.md"] = "---\nprefix: META\n---\n\n# Как мы ведём конвенции\n\n" + versionLine + "\n"
@@ -392,7 +392,7 @@ func TestChecks(t *testing.T) {
}, {
name: "document without a topic carries layer keys",
setup: func(f files) {
f["suite.toml"] = strings.Replace(baseManifest,
f[".conventions-suite.toml"] = strings.Replace(baseManifest,
`TIME = "conventions/time.md"`,
`TIME = "conventions/time.md"`+"\nGTIM = \"conventions/go.md\"", 1)
f["conventions/go.md"] = "---\nprefix: GTIM\nlang: go\n---\n\n# Go\n\n" + versionLine + "\n"
+3 -3
View File
@@ -93,7 +93,7 @@ prefix: SLOG
func layered() files {
return files{
"suite.toml": layeredManifest,
".conventions-suite.toml": layeredManifest,
"LANGUAGE.md": "# Язык конвенций\n\nОписание языка.\n",
"READING.md": reading,
"conventions/arch/time.md": archTime,
@@ -136,7 +136,7 @@ func TestLayeredChecks(t *testing.T) {
}, {
name: "topic with two layers lacking axis keys",
setup: func(f files) {
f["suite.toml"] = strings.Replace(layeredManifest,
f[".conventions-suite.toml"] = strings.Replace(layeredManifest,
`GTIM = "conventions/lang/go/time.md"`,
`GTIM = "conventions/second/time.md"`, 1)
f["conventions/lang/go/time.md"] = ""
@@ -273,7 +273,7 @@ func TestNoFalsePositives(t *testing.T) {
}, {
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,
f[".conventions-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"
+1 -1
View File
@@ -127,7 +127,7 @@ func TestAddInInteractiveMode(t *testing.T) {
t.Errorf("the file lost %q:\n%s", want, body)
}
}
if !strings.Contains(read(t, root, "suite.toml"), `logging = "логирование: уровни"`) {
if !strings.Contains(read(t, root, ".conventions-suite.toml"), `logging = "логирование: уровни"`) {
t.Error("the topic did not reach the manifest")
}
checkClean(t, root)
+2 -2
View File
@@ -144,7 +144,7 @@ func TestRetirePrefixMovesTheNameAndTakesTheFile(t *testing.T) {
t.Error("the file stayed behind")
}
toml := read(t, root, "suite.toml")
toml := read(t, root, ".conventions-suite.toml")
if strings.Contains(toml, `SLOG = "conventions/logging.md"`) {
t.Errorf("the prefix stayed in the live half:\n%s", toml)
}
@@ -179,7 +179,7 @@ func TestRetireTopicWaitsForItsLayers(t *testing.T) {
t.Fatalf("retiring an empty topic returned %d: %s", code, out)
}
toml := read(t, root, "suite.toml")
toml := read(t, root, ".conventions-suite.toml")
if strings.Contains(toml, `time = "время"`) {
t.Errorf("the topic stayed in the live half:\n%s", toml)
}
+1 -1
View File
@@ -28,7 +28,7 @@ particular suite is for and how it is kept.
## The manifest
` + "`suite.toml`" + ` holds the identity of the suite: the language its rules are
` + "`.conventions-suite.toml`" + ` holds the identity of the suite: the language its rules are
written in, its topics and its rule prefixes. It is written by ` + "`convy`" + ` and
carries no comments — a command rewrites the whole file, and a comment would not
survive that. Explanations belong here instead.
+6 -4
View File
@@ -30,9 +30,11 @@ import (
)
// Name is the name of a suite manifest. Which of the two manifests lies next
// to you tells you where you are: suite.toml means a suite, .conventions.toml
// means a project.
const Name = "suite.toml"
// to you tells you where you are, and both start with a dot for the same
// reason: a manifest is data the tool writes, not a document of the repository,
// and it sits with the rest of the service files rather than among the
// conventions themselves.
const Name = ".conventions-suite.toml"
// DefaultLanguageCode is the suite's natural language when the manifest says
// nothing about it. The key is optional on purpose: the vocabulary lives in the
@@ -83,7 +85,7 @@ func (s *Section) Retire(key, note string) {
s.Retired[key] = note
}
// Manifest is a parsed suite.toml.
// Manifest is a parsed suite manifest.
type Manifest struct {
Language Language `toml:"language"`
Topics Section `toml:"topics,omitempty"`
+3 -3
View File
@@ -118,13 +118,13 @@ func TestOpenGitTakesTheCommittedState(t *testing.T) {
t.Skip("no git in PATH")
}
repo := t.TempDir()
name := filepath.Join(repo, "suite.toml")
name := filepath.Join(repo, ".conventions-suite.toml")
if err := os.WriteFile(name, []byte("committed\n"), 0o644); err != nil {
t.Fatal(err)
}
for _, args := range [][]string{
{"init", "--quiet", "-b", "main"},
{"-c", "user.email=t@example.org", "-c", "user.name=t", "add", "suite.toml"},
{"-c", "user.email=t@example.org", "-c", "user.name=t", "add", ".conventions-suite.toml"},
{"-c", "user.email=t@example.org", "-c", "user.name=t", "commit", "--quiet", "-m", "first"},
} {
cmd := exec.Command("git", args...)
@@ -145,7 +145,7 @@ func TestOpenGitTakesTheCommittedState(t *testing.T) {
if err != nil {
t.Fatalf("cloning a local repository: %v", err)
}
body, err := os.ReadFile(filepath.Join(tree.Dir(), "suite.toml"))
body, err := os.ReadFile(filepath.Join(tree.Dir(), ".conventions-suite.toml"))
if err != nil {
t.Fatal(err)
}