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

134 lines
3.6 KiB
Go

package cli
import (
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"git.vakhrushev.me/av/convy/internal/check"
"git.vakhrushev.me/av/convy/internal/manifest"
"git.vakhrushev.me/av/convy/internal/suite"
)
func runSuiteCheck(env Env, args []string) ExitCode {
fs := flag.NewFlagSet("convy suite check", flag.ContinueOnError)
fs.SetOutput(env.Err)
root := fs.String("root", "", "root of the suite; by default it is looked up upwards from the current directory")
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
}
dir := *root
if dir == "" {
found, err := manifest.Find(env.Dir)
if err != nil {
if errors.Is(err, manifest.ErrNotFound) {
fmt.Fprintf(env.Err, "not a conventions suite: no %s here or above\n", manifest.Name)
if _, err := os.Stat(filepath.Join(env.Dir, ".conventions.toml")); err == nil {
fmt.Fprintln(env.Err, "this is a project — checking what is here is called \"convy check\"")
}
return Usage
}
fmt.Fprintln(env.Err, err)
return Usage
}
dir = found
}
s, err := suite.Load(dir)
if err != nil {
fmt.Fprintln(env.Err, err)
return Usage
}
rep := check.Suite(s)
if *asJSON {
if code := printJSON(env, rep); code != OK {
return code
}
} else {
printReport(env.Out, rep, s, *quiet)
}
if rep.Errors() > 0 {
return Failed
}
return OK
}
// printJSON writes the findings for a machine. It is the same report the person
// gets, in the same order — a second answer that disagreed with the first would
// be worse than no second answer.
func printJSON(env Env, rep *check.Report) ExitCode {
body, err := rep.JSON()
if err != nil {
fmt.Fprintln(env.Err, err)
return Failed
}
env.Out.Write(body)
return OK
}
func printReport(w io.Writer, rep *check.Report, s *suite.Suite, quiet bool) {
findings := rep.Findings()
printFindings(w, findings)
if quiet {
return
}
if len(findings) > 0 {
fmt.Fprintln(w)
}
fmt.Fprintf(w, "suite: %s, %s, language version %d (%s)\n",
plural(len(s.Docs), "file"),
plural(len(s.Manifest.LiveTopics()), "topic"),
s.Manifest.Language.Version, s.Manifest.Language.Lang)
// A check that was skipped says so. Reaching the language costs a fetch and
// this check runs on every edit, so the documents about the language go
// unread — and a check silently not run reads exactly like a check passed.
if spec := s.Manifest.Language.Source; spec != "" {
fmt.Fprintf(w, "the language lies at %s and was not fetched: its documents went unchecked\n", spec)
}
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())
default:
fmt.Fprintln(w, "suite integrity holds")
}
}
// plural agrees a noun with a count: 1 file, 4 files.
func plural(n int, noun string) string {
if n == 1 {
return fmt.Sprintf("%d %s", n, noun)
}
return fmt.Sprintf("%d %ss", n, noun)
}
// printFindings lays the findings out grouped by file. Both checks print them
// the same way: a finding of the suite and a finding of a copy are read by the
// same person, and two layouts would be two things to learn.
func printFindings(w io.Writer, findings []check.Finding) {
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)
}
}