Files
convy/internal/cli/check.go
T
av 600aba5ee3 закрыты известные остатки
- документ самоуправления объявляется ключом governance, а не угадывается
  по «он один и без ключей оси»: конвенция, потерявшая topic, была от него
  неотличима и тихо теряла все проверки об отъезде к потребителю
- проверка путей канона больше не ловит README.md и READING.md — эти два
  имени значат что-то и на стороне потребителя
- lang.Recognize требует совпадения и слов, и номера версии; директории
  компонентов сверяются на вложенность, а не только на равенство
- у обеих проверок появился --json, а convy sync называет ссылки на темы,
  которых компонент не взял
2026-07-28 10:16:27 +03:00

163 lines
4.5 KiB
Go

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/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")
asJSON := fs.Bool("json", false, "write the findings as JSON, for a caller that is not a person")
if err := fs.Parse(args); err != nil {
return Usage
}
if code := noStrayArgs(env, "convy check", fs.Args()); code != OK {
return code
}
dir, code := projectRoot(env, *root)
if code != OK {
return code
}
m, code := loadProject(env, dir)
if code != OK {
return code
}
if err := distinctDirs(m); 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 {
c := m.Components[name]
if c.Dir == "" {
fmt.Fprintf(env.Err, "the component %q names no dir, and there is nothing to look in\n", name)
return Usage
}
found, errs := copies(dir, c.Dir)
docs = append(docs, found...)
broken = append(broken, errs...)
}
docs = distinct(docs)
rep := check.Copies(docs)
for _, err := range broken {
fmt.Fprintf(env.Err, "%s\n", err)
}
if *asJSON {
if code := printJSON(env, rep); code != OK {
return code
}
} else {
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
}
// distinct drops a document reached through two components. Directories are
// checked for equality before this, but one may still lie inside another, and a
// finding printed twice reads as two.
func distinct(docs []*doc.Document) []*doc.Document {
seen := make(map[string]bool, len(docs))
out := docs[:0]
for _, d := range docs {
if seen[d.Path] {
continue
}
seen[d.Path] = true
out = append(out, d)
}
return out
}
func printCopyReport(w io.Writer, rep *check.Report, files, comps int, quiet bool) {
findings := rep.Findings()
printFindings(w, findings)
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)
}
}