184 lines
5.3 KiB
Go
184 lines
5.3 KiB
Go
|
|
package layer
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"sort"
|
||
|
|
"strings"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Form is one of the two shapes §11 accepts for a declaration: a layer: key in
|
||
|
|
// INTENT.md frontmatter, or an equivalent declaration file.
|
||
|
|
type Form struct {
|
||
|
|
Source string // path relative to the survey root, "" when the form is absent
|
||
|
|
Layer string // the layer: value exactly as written
|
||
|
|
Role string
|
||
|
|
Found bool
|
||
|
|
// InVocabulary reports whether Layer is in the §3 vocabulary as written.
|
||
|
|
// Deliberately case-sensitive: whether §3 is case-insensitive is the open
|
||
|
|
// question (FLEX-WP-0030 B1), and folding case here would hide it.
|
||
|
|
InVocabulary bool
|
||
|
|
}
|
||
|
|
|
||
|
|
// SurveyRow is one repository's declaration as observed from outside. It holds
|
||
|
|
// BOTH forms, because §11 permits either and does not say which is
|
||
|
|
// authoritative when a repository carries both and they disagree.
|
||
|
|
type SurveyRow struct {
|
||
|
|
Repo string
|
||
|
|
Intent Form // INTENT.md frontmatter
|
||
|
|
File Form // layer.yaml or equivalent
|
||
|
|
}
|
||
|
|
|
||
|
|
// Declared reports whether any machine-readable form was found.
|
||
|
|
func (r SurveyRow) Declared() bool { return r.Intent.Found || r.File.Found }
|
||
|
|
|
||
|
|
// SelfDisagrees reports a repository whose two §11 forms state different layer
|
||
|
|
// values. Case counts: that is the point of the finding, not an artifact of it.
|
||
|
|
func (r SurveyRow) SelfDisagrees() bool {
|
||
|
|
return r.Intent.Found && r.File.Found && r.Intent.Layer != r.File.Layer
|
||
|
|
}
|
||
|
|
|
||
|
|
// Layer returns the value to report, preferring INTENT.md, which §11 names
|
||
|
|
// first. The preference is this survey's, not a ruling — which form governs is
|
||
|
|
// exactly what FLEX-WP-0030 B1 asks.
|
||
|
|
func (r SurveyRow) Layer() string {
|
||
|
|
if r.Intent.Found {
|
||
|
|
return r.Intent.Layer
|
||
|
|
}
|
||
|
|
return r.File.Layer
|
||
|
|
}
|
||
|
|
|
||
|
|
var declarationFiles = []string{"layer.yaml", "layer.yml"}
|
||
|
|
|
||
|
|
// SurveyDeclarations reads both §11 forms for every repository under root.
|
||
|
|
//
|
||
|
|
// It checks ONE property: whether each layer: value sits in the §3 vocabulary
|
||
|
|
// as written. It deliberately does not apply flex-auth's own declaration rules
|
||
|
|
// — pep_stance, tooling_contacts, conformance_record — to any other repository.
|
||
|
|
// Those are flex-auth's invariants for flex-auth, and §11 is explicit that a
|
||
|
|
// layer stated about a repository by another repository is not a declaration.
|
||
|
|
// A survey that graded peers by the surveyor's house rules would be that defect
|
||
|
|
// wearing a tool for a hat.
|
||
|
|
func SurveyDeclarations(root string, repos []string) ([]SurveyRow, error) {
|
||
|
|
var rows []SurveyRow
|
||
|
|
for _, repo := range repos {
|
||
|
|
row := SurveyRow{Repo: repo}
|
||
|
|
row.Intent = readForm(root, filepath.Join(repo, "INTENT.md"))
|
||
|
|
for _, name := range declarationFiles {
|
||
|
|
if f := readForm(root, filepath.Join(repo, name)); f.Found {
|
||
|
|
row.File = f
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
rows = append(rows, row)
|
||
|
|
}
|
||
|
|
sort.Slice(rows, func(i, j int) bool { return rows[i].Repo < rows[j].Repo })
|
||
|
|
return rows, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func readForm(root, rel string) Form {
|
||
|
|
path := filepath.Join(root, rel)
|
||
|
|
if _, err := os.Stat(path); err != nil {
|
||
|
|
return Form{}
|
||
|
|
}
|
||
|
|
decl, err := loadAnyDeclaration(path)
|
||
|
|
if err != nil || strings.TrimSpace(decl.Layer) == "" {
|
||
|
|
return Form{}
|
||
|
|
}
|
||
|
|
return Form{
|
||
|
|
Source: rel,
|
||
|
|
Layer: decl.Layer,
|
||
|
|
Role: decl.Role,
|
||
|
|
Found: true,
|
||
|
|
InVocabulary: validLayers[decl.Layer],
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// loadAnyDeclaration reads INTENT.md frontmatter or a bare declaration file.
|
||
|
|
func loadAnyDeclaration(path string) (Declaration, error) {
|
||
|
|
if strings.HasSuffix(path, ".md") {
|
||
|
|
return LoadDeclaration(path)
|
||
|
|
}
|
||
|
|
body, err := os.ReadFile(path)
|
||
|
|
if err != nil {
|
||
|
|
return Declaration{}, err
|
||
|
|
}
|
||
|
|
return parseDeclarationYAML(string(body))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Spellings groups every observed layer: value across both forms, so the spread
|
||
|
|
// is a result rather than a claim.
|
||
|
|
func Spellings(rows []SurveyRow) map[string][]string {
|
||
|
|
out := map[string][]string{}
|
||
|
|
add := func(v, where string) {
|
||
|
|
if v != "" {
|
||
|
|
out[v] = append(out[v], where)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for _, r := range rows {
|
||
|
|
if r.Intent.Found {
|
||
|
|
add(r.Intent.Layer, r.Repo+"/INTENT.md")
|
||
|
|
}
|
||
|
|
if r.File.Found {
|
||
|
|
add(r.File.Layer, r.File.Source)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
// Undeclared lists repositories with no machine-readable declaration in either form.
|
||
|
|
func Undeclared(rows []SurveyRow) []string {
|
||
|
|
var out []string
|
||
|
|
for _, r := range rows {
|
||
|
|
if !r.Declared() {
|
||
|
|
out = append(out, r.Repo)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
// SelfDisagreeing lists repositories whose two §11 forms disagree.
|
||
|
|
func SelfDisagreeing(rows []SurveyRow) []SurveyRow {
|
||
|
|
var out []SurveyRow
|
||
|
|
for _, r := range rows {
|
||
|
|
if r.SelfDisagrees() {
|
||
|
|
out = append(out, r)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
// FormatSurvey renders the survey as a stable, diffable report.
|
||
|
|
func FormatSurvey(rows []SurveyRow) string {
|
||
|
|
var b strings.Builder
|
||
|
|
fmt.Fprintf(&b, "%-18s %-16s %-16s %s\n", "REPO", "INTENT.md", "DECL FILE", "NOTE")
|
||
|
|
for _, r := range rows {
|
||
|
|
note := ""
|
||
|
|
switch {
|
||
|
|
case !r.Declared():
|
||
|
|
note = "NO DECLARATION (§11)"
|
||
|
|
case r.SelfDisagrees():
|
||
|
|
note = "forms disagree"
|
||
|
|
case !r.Intent.Found:
|
||
|
|
note = "declaration file only"
|
||
|
|
case !r.File.Found:
|
||
|
|
note = "INTENT.md only"
|
||
|
|
}
|
||
|
|
for _, f := range []Form{r.Intent, r.File} {
|
||
|
|
if f.Found && !f.InVocabulary {
|
||
|
|
note += "; outside §3 vocabulary as written"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
fmt.Fprintf(&b, "%-18s %-16s %-16s %s\n", r.Repo, dash(r.Intent.Layer), dash(r.File.Layer), note)
|
||
|
|
}
|
||
|
|
return b.String()
|
||
|
|
}
|
||
|
|
|
||
|
|
func dash(s string) string {
|
||
|
|
if strings.TrimSpace(s) == "" {
|
||
|
|
return "—"
|
||
|
|
}
|
||
|
|
return s
|
||
|
|
}
|