проектные команды и ссылки на источник

- заведён internal/source: уровни ссылаются друг на друга путём на диске
  или git-репозиторием, ревизия закрепляется хвостом #ref; клон делается
  заново и удаляется, кэша нет
- добавлены init, add, pull, list, check в проекте — манифест
  .conventions.toml, сборка копий по разу на компонент, маркер локальной
  части, READING.md рядом
- проверки формы развязаны с набором: принимают lang.Vocabulary, а язык
  копии узнаётся по строке о версии — манифеста рядом с ней нет
This commit is contained in:
av
2026-07-27 20:42:18 +03:00
parent 4615de6e86
commit 23d88c4048
27 changed files with 3009 additions and 57 deletions
+142
View File
@@ -0,0 +1,142 @@
package cli
import (
"flag"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"sort"
"git.vakhrushev.me/av/convy/internal/check"
"git.vakhrushev.me/av/convy/internal/doc"
"git.vakhrushev.me/av/convy/internal/manifest"
"git.vakhrushev.me/av/convy/internal/project"
)
// convy check stays at the top level and reaches for no suite. The form of a
// rule is one and the same, the local rules of the repository on X prefixes are
// written by that same form, and checking what lies here has to work without a
// network and without knowing where the copies came from.
func runCheck(env Env, args []string) ExitCode {
fs := flag.NewFlagSet("convy check", flag.ContinueOnError)
fs.SetOutput(env.Err)
root := fs.String("root", "", "root of the project; it is looked up upwards by default")
forComponent := fs.String("for", "", "component to check; every one of them by default")
quiet := fs.Bool("quiet", false, "print findings only")
if err := fs.Parse(args); err != nil {
return Usage
}
dir, code := projectRoot(env, *root)
if code != OK {
return code
}
m, err := manifest.LoadProject(dir)
if err != nil {
fmt.Fprintln(env.Err, err)
return Usage
}
names, code := components(env, m, *forComponent)
if code != OK {
return code
}
var docs []*doc.Document
var broken []error
for _, name := range names {
found, errs := copies(dir, m.Components[name].Dir)
docs = append(docs, found...)
broken = append(broken, errs...)
}
rep := check.Copies(docs)
for _, err := range broken {
fmt.Fprintf(env.Err, "%s\n", err)
}
printCopyReport(env.Out, rep, len(docs), len(names), *quiet)
if rep.Errors() > 0 || len(broken) > 0 {
return Failed
}
return OK
}
// copies collects the assembled conventions of one component directory.
//
// What is a copy is decided by the origin key rather than by the name of the
// file: README.md belongs to the repository, READING.md belongs to the suite,
// and a file whose origin key was taken away has become a document of the
// repository — none of the three answers to the form of a rule.
func copies(root, dir string) ([]*doc.Document, []error) {
var docs []*doc.Document
var broken []error
base := filepath.Join(root, filepath.FromSlash(dir))
err := filepath.WalkDir(base, func(name string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
if entry.IsDir() || filepath.Ext(entry.Name()) != ".md" {
return nil
}
rel, err := filepath.Rel(root, name)
if err != nil {
return err
}
rel = filepath.ToSlash(rel)
d, err := doc.Load(rel, name)
if err != nil {
broken = append(broken, err)
return nil
}
if d.Front.Origin == "" {
return nil
}
docs = append(docs, d)
return nil
})
if err != nil && !os.IsNotExist(err) {
broken = append(broken, fmt.Errorf("walking %s: %w", dir, err))
}
sort.Slice(docs, func(i, j int) bool { return docs[i].Path < docs[j].Path })
return docs, broken
}
func printCopyReport(w io.Writer, rep *check.Report, files, comps int, quiet bool) {
findings := rep.Findings()
current := ""
for _, f := range findings {
if f.Path != current {
if current != "" {
fmt.Fprintln(w)
}
fmt.Fprintf(w, "%s\n", f.Path)
current = f.Path
}
where := ""
if f.Line > 0 {
where = fmt.Sprintf(":%d", f.Line)
}
fmt.Fprintf(w, " %s%s %s [%s]\n", f.Severity, where, f.Msg, f.Family)
}
if quiet {
return
}
if len(findings) > 0 {
fmt.Fprintln(w)
}
fmt.Fprintf(w, "project: %s in %s\n", plural(files, "file"), plural(comps, "component"))
switch {
case rep.Errors() > 0:
fmt.Fprintf(w, "errors: %d, warnings: %d\n", rep.Errors(), rep.Warnings())
case rep.Warnings() > 0:
fmt.Fprintf(w, "no errors, warnings: %d\n", rep.Warnings())
case files == 0:
fmt.Fprintf(w, "nothing to check: no file carries an origin key; convy pull assembles the copies\n")
default:
fmt.Fprintf(w, "the copies hold the form; everything below %s is the repository's own\n", project.LocalMarker)
}
}