flex-auth/internal/layer/survey.go
tegwick e0c6c4389d
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 1m11s
Widen A12 enforcement from the key name to the declaration's content (GH-DEC-2026-020).
internal/layer/conformance.go enforced A12 as "no key named standard_version",
and so could not see the same pin as a versioned standard: path or as
companion_version. It now detects a version of the standard or its companion
in any key or value of the declaration (INTENT.md frontmatter, layer.yaml),
including a version in a path, and excludes comments and schema_version. It
refuses to be applied to pep-stance.yaml, pip-claims.yaml or
evidence-classification.yaml, which A12 r2 does not reach (§3).

Every run of check_layer_conformance and of the estate survey now prints the
standard version it checks against (layer.ValidatedAgainst, kings-guard's
pattern) and its scope (§4). The survey applies the same detection to peers'
declarations; the receipt is refreshed because the survey's output changed
(no peer declaration currently carries a version).

Tests fail if a versioned standard: path or a companion_version comes back.
flex-auth's own INTENT.md needed no change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 63291@bnt-lap001
Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
2026-09-21 09:39:46 +02:00

350 lines
12 KiB
Go

package layer
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"gopkg.in/yaml.v3"
)
// 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 after ASCII
// case folding. GH-DEC-2026-017 §2 ruled comparison case-insensitive and
// requires a run to fold before comparing, so a lowercase declaration is
// conforming rather than tolerated. The earlier field was deliberately
// case-sensitive while that was the open question; it is now answered.
InVocabulary bool
// Canonical is the §4 column spelling of Layer, empty when out of vocabulary.
Canonical string
// VersionPins lists every key or value of this declaration form carrying a
// version of the standard or its companion (A12 r2, GH-DEC-2026-020 §1-§2).
// Read from the declaration only — never from a stance, claims or
// classification map, which A12 r2 does not reach (§3).
VersionPins []string `json:",omitempty"`
}
// 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
// InCatalog reports whether this repository is a §4 row. §11 binds §4, so a
// row that is false can be a volunteer but cannot be a §11 non-conformance.
InCatalog bool
}
// 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 as written. Under GH-DEC-2026-017 §1 a disagreement between the two
// forms is a finding IN ITS OWN RIGHT and MUST be reported rather than resolved
// away by precedence: precedence says which value is the repository's answer,
// it does not say the disagreement did not happen. So case still counts here.
func (r SurveyRow) SelfDisagrees() bool {
return r.Intent.Found && r.File.Found && r.Intent.Layer != r.File.Layer
}
// DisagreesOnLayer reports the stronger case: the two forms name different
// LAYERS, not merely different spellings of one. Case-folded per A9. Nine of
// the estate's nine self-disagreements are spelling only, and none of them is
// this.
func (r SurveyRow) DisagreesOnLayer() bool {
return r.Intent.Found && r.File.Found && r.Intent.Canonical != r.File.Canonical
}
// Layer returns the repository's answer. INTENT.md governs: GH-DEC-2026-017 §1
// rules that a layer.yaml or equivalent is a DERIVED artifact under §11's own
// derived-artifact rule — marked derived, naming INTENT.md, required to agree —
// and that the declaration-form paragraph gave the declaration a machine-readable
// FORM, not a second AUTHORITY. That is now a ruling, no longer this survey's
// preference.
func (r SurveyRow) Layer() string {
if r.Intent.Found {
return r.Intent.Layer
}
return r.File.Layer
}
var declarationFiles = []string{"layer.yaml", "layer.yml"}
// catalogRows are §4's layer-catalog repositories. §11's obligations attach to
// estate-authored repositories IN §4; a repository outside it may declare
// voluntarily, and a run that grades a volunteer is over-scoped
// (GH-DEC-2026-017 §4, amendment A11). `OpenBao` is a §4 row but is not
// estate-authored, so no declaration is owed there.
var catalogRows = map[string]bool{
"info-tech-canon": true, "net-kingdom": true, "key-cape": true,
"user-engine": true, "tenant-engine": true, "zone-engine": true,
"secrets-engine": true, "audit-core": true, "access-engine": true,
"approval-engine": true, "maturity-engine": true, "gate-house": true,
"ops-mason": true, "ops-warden": true, "kings-guard": true,
"whitehat-security": true,
}
// catalogAliases maps a checkout directory to the §4 row it is. The rename to
// access-engine is ruled and sequenced (FLEX-DEC-2026-013, FLEX-WP-0020) and
// has not landed; §4's own note says both names denote the same authority until
// it does.
var catalogAliases = map[string]string{"flex-auth": "access-engine"}
// InCatalog reports whether a checkout directory is a §4 catalog row.
func InCatalog(repo string) bool {
if alias, ok := catalogAliases[repo]; ok {
repo = alias
}
return catalogRows[repo]
}
// Scope is what a conformance run ranged over.
//
// §11 as amended by A11 requires a run to STATE its scope: a run over §4 and a
// run over every repository carrying a declaration answer different questions,
// and a report that does not say which it did cannot be acted on. That is the
// same defect as a survey that did not record which file a value came from —
// the defect this survey was built to remove — one level up.
type Scope struct {
Name string
Statement string
Repos []string
}
// CatalogScope is a run over the §4 catalog: the set §11's obligations bind.
func CatalogScope(repos []string) Scope {
var in []string
for _, r := range repos {
if InCatalog(r) {
in = append(in, r)
}
}
return Scope{
Name: "§4 catalog",
Statement: "estate-authored repositories catalogued in §4. §11's obligations attach here, and only here.",
Repos: in,
}
}
// EstateScope is a run over every repository asked, catalogued or not. It
// answers a different question from CatalogScope and says so: a repository
// outside §4 that declares is reported as a volunteer, never as a §11
// non-conformance.
func EstateScope(repos []string) Scope {
return Scope{
Name: "estate-wide",
Statement: "every repository surveyed, catalogued or not. Repositories outside §4 are reported as declared voluntarily, outside catalog scope — never as §11 non-conformances.",
Repos: append([]string(nil), repos...),
}
}
// SurveyDeclarations reads both §11 forms for every repository under root.
//
// It checks TWO properties, both §11 statute rather than house rule: whether
// each layer: value sits in the §3 vocabulary (case folded), and whether the
// declaration carries a standard or companion version anywhere (A12 r2). It
// reads INTENT.md frontmatter and layer.yaml only, so a peer's pep-stance.yaml,
// pip-claims.yaml or evidence-classification.yaml is never graded. 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
}
}
row.InCatalog = InCatalog(repo)
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{}
}
canon, ok := CanonicalLayer(decl.Layer)
pins, _ := DeclarationVersionPins(path)
return Form{
VersionPins: pins,
Source: rel,
Layer: decl.Layer,
Role: decl.Role,
Found: true,
InVocabulary: ok,
Canonical: canon,
}
}
// peerDeclaration is the ONLY shape the survey reads from another repository:
// layer and role. Decoding a peer into flex-auth's own Declaration applied
// flex-auth's field types to someone else's file — and when flex-auth's
// emission_guarantee became a path string, audit-core's list-shaped
// emission_guarantee failed to decode and its layer.yaml silently vanished
// from the survey. A surveyor's schema is a house rule too.
type peerDeclaration struct {
Layer string `yaml:"layer"`
Role string `yaml:"role"`
}
// loadAnyDeclaration reads INTENT.md frontmatter or a bare declaration file.
func loadAnyDeclaration(path string) (peerDeclaration, error) {
body, err := os.ReadFile(path)
if err != nil {
return peerDeclaration{}, err
}
doc := string(body)
if strings.HasSuffix(path, ".md") {
if doc, err = splitFrontmatter(doc); err != nil {
return peerDeclaration{}, err
}
}
var decl peerDeclaration
if err := yaml.Unmarshal([]byte(doc), &decl); err != nil {
return peerDeclaration{}, fmt.Errorf("parse layer declaration %s: %w", path, err)
}
return decl, nil
}
// 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
}
// VersionPinned lists each declaration form carrying a standard or companion
// version, as "source: finding".
func VersionPinned(rows []SurveyRow) []string {
var out []string
for _, r := range rows {
for _, f := range []Form{r.Intent, r.File} {
for _, p := range f.VersionPins {
out = append(out, f.Source+": "+p)
}
}
}
return out
}
// VolunteerDeclarations lists repositories outside §4 that declared anyway. The
// correct report for them is "declared voluntarily, outside catalog scope"
// (GH-DEC-2026-017 §4): treating a volunteer's declaration as a non-conformance
// is both wrong about who §11 binds and the fastest way to stop getting
// volunteers.
func VolunteerDeclarations(rows []SurveyRow) []string {
var out []string
for _, r := range rows {
if !r.InCatalog && r.Declared() {
out = append(out, r.Repo)
}
}
return out
}
// FormatSurvey renders the survey as a stable, diffable report. It leads with
// the scope because §11 requires a run to state what it ranged over.
func FormatSurvey(scope Scope, rows []SurveyRow) string {
var b strings.Builder
fmt.Fprintf(&b, "Validated against: %s\n", ValidatedAgainst)
fmt.Fprintf(&b, "Scope: %s — %s\n", scope.Name, scope.Statement)
fmt.Fprintf(&b, "Reads: INTENT.md frontmatter and layer.yaml/layer.yml per repository; no stance, claims or classification map is read.\n\n")
fmt.Fprintf(&b, "%-18s %-16s %-16s %s\n", "REPO", "INTENT.md", "DECL FILE", "NOTE")
for _, r := range rows {
note := ""
switch {
case !r.Declared() && !r.InCatalog:
note = "no declaration; outside §4 — none owed"
case !r.Declared():
note = "NO DECLARATION (§11)"
case r.DisagreesOnLayer():
note = "forms name DIFFERENT LAYERS; INTENT.md governs"
case r.SelfDisagrees():
note = "forms disagree on spelling only; INTENT.md governs, still a finding"
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 += "; " + f.Layer + " is outside the closed §3 vocabulary"
}
}
for _, f := range []Form{r.Intent, r.File} {
if len(f.VersionPins) > 0 {
note += "; " + f.Source + " carries a version (A12 r2)"
}
}
if r.Declared() && !r.InCatalog {
note += "; declared voluntarily, outside §4 catalog scope"
}
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
}