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 }