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

139 lines
3.7 KiB
Go

// Package check runs the integrity checks of a suite.
//
// The split into families is taken from the language and kept in the code: the
// form of a rule is checked in any file the language employs; spread only in
// convention files, because those checks are about a document travelling to a
// consumer. The third part of the list — rows of a table being mutually
// exclusive, the scope being covered, a norm being self-sufficient — is not
// here: it does not yield to parsing text and stays the reader's work.
package check
import (
"encoding/json"
"fmt"
"sort"
)
// Severity tells an error from a warning. An error is a violation named by a
// rule of the suite; a warning is something worth a look.
type Severity int
const (
Error Severity = iota + 1
Warning
)
func (s Severity) String() string {
if s == Warning {
return "warning"
}
return "error"
}
// Family is the family of checks a finding came from.
type Family string
const (
Manifest Family = "manifest"
Form Family = "form"
Spread Family = "spread"
Links Family = "links"
)
// Finding is a single finding.
type Finding struct {
Severity Severity
Family Family
// Path is the path of the file from the root of the suite; empty when the
// finding is about the suite as a whole.
Path string
// Line is the line of the file; zero when the finding is not bound to one.
Line int
Msg string
}
// Report accumulates the findings of one run.
type Report struct {
findings []Finding
}
// Errorf records an error.
func (r *Report) Errorf(f Family, path string, line int, format string, args ...any) {
r.add(Error, f, path, line, format, args...)
}
// Warnf records a warning.
func (r *Report) Warnf(f Family, path string, line int, format string, args ...any) {
r.add(Warning, f, path, line, format, args...)
}
func (r *Report) add(s Severity, f Family, path string, line int, format string, args ...any) {
r.findings = append(r.findings, Finding{
Severity: s,
Family: f,
Path: path,
Line: line,
Msg: fmt.Sprintf(format, args...),
})
}
// Findings hands over the findings ordered by file and line.
func (r *Report) Findings() []Finding {
out := make([]Finding, len(r.findings))
copy(out, r.findings)
sort.SliceStable(out, func(i, j int) bool {
if out[i].Path != out[j].Path {
return out[i].Path < out[j].Path
}
return out[i].Line < out[j].Line
})
return out
}
// Errors counts the findings of error severity.
func (r *Report) Errors() int {
n := 0
for _, f := range r.findings {
if f.Severity == Error {
n++
}
}
return n
}
// Warnings counts the warnings.
func (r *Report) Warnings() int {
return len(r.findings) - r.Errors()
}
// MarshalJSON writes a finding the way a machine reads it: the severity and the
// family as words rather than as the numbers they happen to be inside.
func (f Finding) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Severity string `json:"severity"`
Family string `json:"family"`
Path string `json:"path,omitempty"`
Line int `json:"line,omitempty"`
Message string `json:"message"`
}{f.Severity.String(), string(f.Family), f.Path, f.Line, f.Msg})
}
// JSON renders the report for a caller that is not a person. Findings come out
// in the order they are printed in, so the two outputs never disagree about
// what was found first.
func (r *Report) JSON() ([]byte, error) {
out := struct {
Findings []Finding `json:"findings"`
Errors int `json:"errors"`
Warnings int `json:"warnings"`
}{r.Findings(), r.Errors(), r.Warnings()}
if out.Findings == nil {
out.Findings = []Finding{}
}
body, err := json.Marshal(out)
if err != nil {
return nil, err
}
return append(body, '\n'), nil
}