манифесты стали данными, заведён 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
+68 -10
View File
@@ -1,12 +1,24 @@
// Package manifest reads suite.toml, the manifest of a conventions suite.
// Package manifest reads and writes the two manifests of the model.
//
// The manifest declares three things: the language the suite's rules are
// The suite manifest declares three things: the language the suite's rules are
// written in, its live and retired topics, and its live and retired rule
// prefixes together with the paths of their files. The tool knows no topic and
// no prefix in advance — that whole list arrives from here.
//
// Both manifests are data the tool edits, so both are decoded into structs and
// written back out of them. They carry no comments: a file a machine rewrites
// cannot keep a comment through the round trip, and pretending otherwise costs
// the comment on a day nobody is watching. What a topic is for is said in the
// documents next to the manifest, which no command touches.
//
// Because a write goes out of the structs, a key the tool does not know would
// disappear on the next edit. So it does not write at all while one is there:
// a refusal naming the key is the only outcome that neither loses it nor hides
// it.
package manifest
import (
"bytes"
"errors"
"fmt"
"os"
@@ -38,25 +50,44 @@ const DefaultLanguageCode = "ru"
// changing — the vocabulary is picked by version and code either way.
type Language struct {
Version int `toml:"version"`
Lang string `toml:"lang"`
Source string `toml:"source"`
Description string `toml:"description"`
Reading string `toml:"reading"`
Lang string `toml:"lang,omitempty"`
Source string `toml:"source,omitempty"`
Description string `toml:"description,omitempty"`
Reading string `toml:"reading,omitempty"`
}
// Section is a part of the manifest split into a live and a retired half.
// Retired entries are kept rather than deleted: a topic name and a rule prefix
// live on in foreign repositories, and neither may ever be reused.
type Section struct {
Live map[string]string `toml:"live"`
Retired map[string]string `toml:"retired"`
Live map[string]string `toml:"live,omitempty"`
Retired map[string]string `toml:"retired,omitempty"`
}
// Add puts an entry into the live half, making the map if there is none.
func (s *Section) Add(key, value string) {
if s.Live == nil {
s.Live = make(map[string]string)
}
s.Live[key] = value
}
// Retire moves an entry out of the live half into the retired one. A name is
// never deleted and never reissued: it lives on in foreign repositories, and a
// name handed out twice starts pointing at something else there.
func (s *Section) Retire(key, note string) {
delete(s.Live, key)
if s.Retired == nil {
s.Retired = make(map[string]string)
}
s.Retired[key] = note
}
// Manifest is a parsed suite.toml.
type Manifest struct {
Language Language `toml:"language"`
Topics Section `toml:"topics"`
Prefixes Section `toml:"prefixes"`
Topics Section `toml:"topics,omitempty"`
Prefixes Section `toml:"prefixes,omitempty"`
// Path is where the manifest was read from.
Path string `toml:"-"`
@@ -91,6 +122,33 @@ func Load(root string) (*Manifest, error) {
return &m, nil
}
// Save writes the manifest back to where it was read from.
func (m *Manifest) Save() error {
return save(m.Path, m, m.Undecoded)
}
// save encodes a manifest and puts it in place.
func save(path string, value any, undecoded []string) error {
if len(undecoded) > 0 {
return fmt.Errorf("%s holds %s the tool does not know (%s); a write goes out of what the tool understands, so the key would be dropped — fix the spelling first",
path, plural(len(undecoded), "key"), strings.Join(undecoded, ", "))
}
var b bytes.Buffer
enc := toml.NewEncoder(&b)
enc.Indent = ""
if err := enc.Encode(value); err != nil {
return fmt.Errorf("encoding %s: %w", path, err)
}
return os.WriteFile(path, b.Bytes(), 0o644)
}
func plural(n int, noun string) string {
if n == 1 {
return fmt.Sprintf("%d %s", n, noun)
}
return fmt.Sprintf("%d %ss", n, noun)
}
// Find walks up from start looking for a directory that holds a suite
// manifest, so that `convy suite check` works from any subdirectory of a suite.
func Find(start string) (string, error) {