манифесты стали данными, заведён convy sync

- убраны комментарии из suite.toml и .conventions.toml: файл, который
  машина переписывает, комментарий через круг не проносит; объяснения
  ушли в README рядом, который suite init теперь заводит
- удалена текстовая правка манифеста целиком — 520 строк ручного
  лексера TOML вместе со всем классом ошибок порчи данных
- запись идёт из структур энкодером; ключ, которого инструмент не
  знает, запись останавливает, а не теряется молча
- convy sync сверяет манифест и подводит под него раскладку файлов:
  чего не хватает — собирает, что осиротело — удаляет, копию с
  локальной частью не трогает никогда
This commit is contained in:
av
2026-07-28 09:45:10 +03:00
parent b6b0976c19
commit 92bd1f463d
18 changed files with 851 additions and 1055 deletions
+59 -39
View File
@@ -10,52 +10,56 @@ import (
"git.vakhrushev.me/av/convy/internal/manifest"
)
// manifestSkeleton is what a suite starts as. The tables stand empty but
// present: `convy suite add` splices entries into them, and a table that is not
// there is a table an edit cannot find.
// A suite starts as a manifest holding only what it knows about itself, and a
// README next to it holding everything a person needs to know to fill it in.
//
// The two documents about the language are left commented out on purpose. The
// suite is expected to carry them, but the tool cannot author them, and a
// manifest pointing at a file that does not exist is a manifest that fails its
// own check on the first run.
const manifestSkeleton = `# The manifest of a conventions suite.
#
# Two manifests exist in the model, each named after what it describes:
# suite.toml here, in the suite, describes the suite itself; .conventions.toml
# in a project describes what that project subscribed to. Which of the two lies
# next to you tells you where you are.
// The two are split because the manifest is data the tool rewrites on every
// `suite add` and every `suite retire`, while the README is prose nothing
// touches. Keeping the explanations inside the manifest would mean losing them
// the first time a command wrote the file.
//
// The README is a starting point, in the tool's own language, for its author to
// replace. What the suite itself is about, only its author knows.
const readmeSkeleton = `# Conventions suite
[language]
version = %d
lang = "%s"
# description = "LANGUAGE.md" # the full account of the language, stays with the author
# reading = "READING.md" # the short guide for a reader, travels into every copy
The rules live in ` + "`conventions/`" + `. What each file is and what it may do is
settled by the conventions language; this README is the place to say what this
particular suite is for and how it is kept.
# ─── Topics ─────────────────────────────────────────────────────────────────
#
# A topic is a set of rules about one focus of development, and the unit of
# subscription. The value is the one line about what the topic is for; the
# table of conventions in a consumer's README is built out of it.
## The manifest
[topics.live]
` + "`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.
# Retired names land here together with a reason and a date, so that they can
# never be handed to another topic: the name lives on in foreign repositories.
## Topics
[topics.retired]
A topic is a set of rules about one focus of development — time, configuration,
the database schema — and it is the unit of subscription: a consumer takes it
whole. A name is never renamed and never reissued, because it lives on in
foreign repositories: in the ` + "`origin:`" + ` header of every copy and in the
subscription of every consumer.
# ─── Rule prefixes ──────────────────────────────────────────────────────────
#
# A prefix is four uppercase Latin letters, unique across the suite, chosen for
# a file rather than derived by a formula. The letter X is reserved for the
# local rules of consumers and is never taken here. Paths are given from the
# root of the repository.
Since the name is permanent, a topic is named after a decision and whom it
addresses rather than after the role some part of today's project plays.
[prefixes.live]
## Prefixes
# Prefixes of deleted and split files land here, likewise never to be reissued.
A prefix is four uppercase Latin letters, unique across the suite, chosen for a
file rather than derived by a formula. Rules are addressed by identifier —
` + "`KEYS-5`" + ` — with no path, so the identifier survives a file moving between
axes. A prefix is never reissued either.
[prefixes.retired]
The letter X in first position is reserved for the local rules of consuming
repositories. The suite never takes it, so a local prefix can never collide with
a future one here.
## Retirement
Nothing leaves the manifest. A topic or a prefix that is done moves to the
retired half together with a reason and a date, so that the name can never be
handed to something else.
`
func runSuiteInit(env Env, args []string) ExitCode {
@@ -108,17 +112,33 @@ func runSuiteInit(env Env, args []string) ExitCode {
return Usage
}
body := fmt.Sprintf(manifestSkeleton, *version, given["lang"])
name := filepath.Join(root, manifest.Name)
if err := os.WriteFile(name, []byte(body), 0o644); err != nil {
m := &manifest.Manifest{
Language: manifest.Language{Version: *version, Lang: given["lang"]},
Path: name,
}
if err := m.Save(); err != nil {
fmt.Fprintln(env.Err, err)
return Usage
}
fmt.Fprintf(env.Out, "\ncreated %s\ncreated %s\n", name, filepath.Join(root, "conventions"))
// The README is written only when there is none: it is a starting point,
// and a starting point that overwrites what somebody already wrote is not
// one.
readme := filepath.Join(root, "README.md")
if !exists(readme) {
if err := os.WriteFile(readme, []byte(readmeSkeleton), 0o644); err != nil {
fmt.Fprintln(env.Err, err)
return Usage
}
fmt.Fprintf(env.Out, "created %s\n", readme)
}
fmt.Fprintf(env.Out, "\nthe suite speaks %s, conventions language version %d\n", given["lang"], *version)
fmt.Fprint(env.Out, `
next:
rewrite README.md — it says what a topic and a prefix are, not what this suite is for
write the two documents about the language and name them in [language]
convy suite add add the first convention
convy suite check verify the suite holds together