diff --git a/docs/conformance/boundaries-review.md b/docs/conformance/boundaries-review.md index 3781aa6..ef6fce8 100644 --- a/docs/conformance/boundaries-review.md +++ b/docs/conformance/boundaries-review.md @@ -50,7 +50,7 @@ Full statements in `workplans/FLEX-WP-0030-boundary-declaration-cleanup.md` T04. | # | Finding | Owner | | --- | --- | --- | -| B1 | `layer:` casing is inconsistent across ten declarations; §11 calls the check mechanical, and flex-auth's own validator would fail eight of them | `gate-house` | +| B1 | **Corrected 2026-09-21.** Nine of nine repositories carrying both §11 forms declare a different `layer:` value in each. §11 does not say which form governs | `gate-house` | | B2 | `gate-house`, `key-cape`, `ops-mason`, `net-kingdom` carry no machine-readable layer declaration | each named repository | | B3 | flex-auth declares no emission guarantee; whether it owes one turns on an unruled boundary with `audit-core` | `gate-house`, `audit-core` | | B4 | a layer declaration should not pin a standard version; if §11 agrees it should say so generally | `gate-house` | @@ -66,3 +66,47 @@ Full statements in `workplans/FLEX-WP-0030-boundary-declaration-cleanup.md` T04. the thing that changes. - **Not a grade.** §9.3's two-owner split is flex-auth's own finding and it cuts here: the PDP does not get to score the repositories whose facts it consumes. + +## Correction — B1, 2026-09-21 + +**The first published version of B1 was wrong, and it was wrong in a way this +review exists to prevent.** + +It reported that the estate spelled `layer:` three ways *across* repositories, +and named eight as non-conformant on casing. That came from an ad-hoc shell +survey run while writing this document. The survey took the first `^layer:` match +per repository across two files without recording which file it came from, so it +reported one value where there were two. + +Re-run mechanically (`tools/survey_layer_declarations.go`, receipt at +`docs/evidence/2026-09-21-layer-declaration-survey.json`), the finding is +different and stronger: + +| | | +| --- | --- | +| Repositories carrying **both** §11 forms | 9 | +| Of those, forms that **disagree** | **9 of 9** | +| Shape | `INTENT.md` says `Engine` / `Staff`; `layer.yaml` says `engine` / `staff` | +| Repositories with only one form | 1 — `flex-auth`, which has no `layer.yaml` | + +The disagreement is **within** each repository, between the two forms §11 +permits, and it is universal rather than sloppy — two generators, two +conventions, no repository mixing them. Nobody is inconsistent with anybody else. + +That relocates the question. §11 accepts *"a `layer:` key in the `INTENT.md` +frontmatter, **or** an equivalent declaration file"* and does not say which +governs when a repository carries both and they disagree. A conformance run +reading `INTENT.md` and one reading `layer.yaml` reach different answers for nine +repositories, and both are following §11. + +flex-auth is the only declared repository that cannot exhibit this, and only +because it never wrote the second file — not a virtue. + +**What this cost.** The wrong version went to `gate-house` and five engine +repositories before it was checked. Corrections were sent the same day. The +review had already said a mechanical check nobody can re-run is an assertion, and +then asserted B1 from a shell pipeline — which is why B1 is now a command with +tests and a receipt, and why this correction is recorded here rather than edited +away. A published review corrected silently is `FLEX-DEC-2026-008`'s defect, and +that rule does not have an exception for the reviewer. + diff --git a/docs/evidence/2026-09-21-layer-declaration-survey.json b/docs/evidence/2026-09-21-layer-declaration-survey.json new file mode 100644 index 0000000..0fbd938 --- /dev/null +++ b/docs/evidence/2026-09-21-layer-declaration-survey.json @@ -0,0 +1,446 @@ +{ + "checks_only": "§3 vocabulary as written; no flex-auth house rules applied to peers", + "derived_at": "run time", + "off_vocab": [ + "approval-engine/layer.yaml=\"engine\"", + "audit-core/layer.yaml=\"engine\"", + "kings-guard/layer.yaml=\"staff\"", + "maturity-engine/layer.yaml=\"engine\"", + "ops-warden/layer.yaml=\"staff\"", + "secrets-engine/layer.yaml=\"engine\"", + "tenant-engine/layer.yaml=\"engine\"", + "user-engine/layer.yaml=\"engine\"", + "zone-engine/layer.yaml=\"engine\"" + ], + "root": "/home/worsch", + "rows": [ + { + "Repo": "approval-engine", + "Intent": { + "Source": "approval-engine/INTENT.md", + "Layer": "Engine", + "Role": "PIP", + "Found": true, + "InVocabulary": true + }, + "File": { + "Source": "approval-engine/layer.yaml", + "Layer": "engine", + "Role": "pip", + "Found": true, + "InVocabulary": false + } + }, + { + "Repo": "audit-core", + "Intent": { + "Source": "audit-core/INTENT.md", + "Layer": "Engine", + "Role": "Evidence", + "Found": true, + "InVocabulary": true + }, + "File": { + "Source": "audit-core/layer.yaml", + "Layer": "engine", + "Role": "evidence", + "Found": true, + "InVocabulary": false + } + }, + { + "Repo": "flex-auth", + "Intent": { + "Source": "flex-auth/INTENT.md", + "Layer": "Engine", + "Role": "PDP", + "Found": true, + "InVocabulary": true + }, + "File": { + "Source": "", + "Layer": "", + "Role": "", + "Found": false, + "InVocabulary": false + } + }, + { + "Repo": "gate-house", + "Intent": { + "Source": "", + "Layer": "", + "Role": "", + "Found": false, + "InVocabulary": false + }, + "File": { + "Source": "", + "Layer": "", + "Role": "", + "Found": false, + "InVocabulary": false + } + }, + { + "Repo": "key-cape", + "Intent": { + "Source": "", + "Layer": "", + "Role": "", + "Found": false, + "InVocabulary": false + }, + "File": { + "Source": "", + "Layer": "", + "Role": "", + "Found": false, + "InVocabulary": false + } + }, + { + "Repo": "kings-guard", + "Intent": { + "Source": "kings-guard/INTENT.md", + "Layer": "Staff", + "Role": "", + "Found": true, + "InVocabulary": true + }, + "File": { + "Source": "kings-guard/layer.yaml", + "Layer": "staff", + "Role": "", + "Found": true, + "InVocabulary": false + } + }, + { + "Repo": "maturity-engine", + "Intent": { + "Source": "maturity-engine/INTENT.md", + "Layer": "Engine", + "Role": "PIP", + "Found": true, + "InVocabulary": true + }, + "File": { + "Source": "maturity-engine/layer.yaml", + "Layer": "engine", + "Role": "pip", + "Found": true, + "InVocabulary": false + } + }, + { + "Repo": "net-kingdom", + "Intent": { + "Source": "", + "Layer": "", + "Role": "", + "Found": false, + "InVocabulary": false + }, + "File": { + "Source": "", + "Layer": "", + "Role": "", + "Found": false, + "InVocabulary": false + } + }, + { + "Repo": "ops-mason", + "Intent": { + "Source": "", + "Layer": "", + "Role": "", + "Found": false, + "InVocabulary": false + }, + "File": { + "Source": "", + "Layer": "", + "Role": "", + "Found": false, + "InVocabulary": false + } + }, + { + "Repo": "ops-warden", + "Intent": { + "Source": "ops-warden/INTENT.md", + "Layer": "Staff", + "Role": "", + "Found": true, + "InVocabulary": true + }, + "File": { + "Source": "ops-warden/layer.yaml", + "Layer": "staff", + "Role": "", + "Found": true, + "InVocabulary": false + } + }, + { + "Repo": "secrets-engine", + "Intent": { + "Source": "secrets-engine/INTENT.md", + "Layer": "Engine", + "Role": "Lifecycle", + "Found": true, + "InVocabulary": true + }, + "File": { + "Source": "secrets-engine/layer.yaml", + "Layer": "engine", + "Role": "lifecycle", + "Found": true, + "InVocabulary": false + } + }, + { + "Repo": "tenant-engine", + "Intent": { + "Source": "tenant-engine/INTENT.md", + "Layer": "Engine", + "Role": "PIP", + "Found": true, + "InVocabulary": true + }, + "File": { + "Source": "tenant-engine/layer.yaml", + "Layer": "engine", + "Role": "pip", + "Found": true, + "InVocabulary": false + } + }, + { + "Repo": "user-engine", + "Intent": { + "Source": "user-engine/INTENT.md", + "Layer": "Engine", + "Role": "PIP", + "Found": true, + "InVocabulary": true + }, + "File": { + "Source": "user-engine/layer.yaml", + "Layer": "engine", + "Role": "pip", + "Found": true, + "InVocabulary": false + } + }, + { + "Repo": "zone-engine", + "Intent": { + "Source": "zone-engine/INTENT.md", + "Layer": "Engine", + "Role": "PIP", + "Found": true, + "InVocabulary": true + }, + "File": { + "Source": "zone-engine/layer.yaml", + "Layer": "engine", + "Role": "pip", + "Found": true, + "InVocabulary": false + } + } + ], + "self_disagreeing": [ + { + "Repo": "approval-engine", + "Intent": { + "Source": "approval-engine/INTENT.md", + "Layer": "Engine", + "Role": "PIP", + "Found": true, + "InVocabulary": true + }, + "File": { + "Source": "approval-engine/layer.yaml", + "Layer": "engine", + "Role": "pip", + "Found": true, + "InVocabulary": false + } + }, + { + "Repo": "audit-core", + "Intent": { + "Source": "audit-core/INTENT.md", + "Layer": "Engine", + "Role": "Evidence", + "Found": true, + "InVocabulary": true + }, + "File": { + "Source": "audit-core/layer.yaml", + "Layer": "engine", + "Role": "evidence", + "Found": true, + "InVocabulary": false + } + }, + { + "Repo": "kings-guard", + "Intent": { + "Source": "kings-guard/INTENT.md", + "Layer": "Staff", + "Role": "", + "Found": true, + "InVocabulary": true + }, + "File": { + "Source": "kings-guard/layer.yaml", + "Layer": "staff", + "Role": "", + "Found": true, + "InVocabulary": false + } + }, + { + "Repo": "maturity-engine", + "Intent": { + "Source": "maturity-engine/INTENT.md", + "Layer": "Engine", + "Role": "PIP", + "Found": true, + "InVocabulary": true + }, + "File": { + "Source": "maturity-engine/layer.yaml", + "Layer": "engine", + "Role": "pip", + "Found": true, + "InVocabulary": false + } + }, + { + "Repo": "ops-warden", + "Intent": { + "Source": "ops-warden/INTENT.md", + "Layer": "Staff", + "Role": "", + "Found": true, + "InVocabulary": true + }, + "File": { + "Source": "ops-warden/layer.yaml", + "Layer": "staff", + "Role": "", + "Found": true, + "InVocabulary": false + } + }, + { + "Repo": "secrets-engine", + "Intent": { + "Source": "secrets-engine/INTENT.md", + "Layer": "Engine", + "Role": "Lifecycle", + "Found": true, + "InVocabulary": true + }, + "File": { + "Source": "secrets-engine/layer.yaml", + "Layer": "engine", + "Role": "lifecycle", + "Found": true, + "InVocabulary": false + } + }, + { + "Repo": "tenant-engine", + "Intent": { + "Source": "tenant-engine/INTENT.md", + "Layer": "Engine", + "Role": "PIP", + "Found": true, + "InVocabulary": true + }, + "File": { + "Source": "tenant-engine/layer.yaml", + "Layer": "engine", + "Role": "pip", + "Found": true, + "InVocabulary": false + } + }, + { + "Repo": "user-engine", + "Intent": { + "Source": "user-engine/INTENT.md", + "Layer": "Engine", + "Role": "PIP", + "Found": true, + "InVocabulary": true + }, + "File": { + "Source": "user-engine/layer.yaml", + "Layer": "engine", + "Role": "pip", + "Found": true, + "InVocabulary": false + } + }, + { + "Repo": "zone-engine", + "Intent": { + "Source": "zone-engine/INTENT.md", + "Layer": "Engine", + "Role": "PIP", + "Found": true, + "InVocabulary": true + }, + "File": { + "Source": "zone-engine/layer.yaml", + "Layer": "engine", + "Role": "pip", + "Found": true, + "InVocabulary": false + } + } + ], + "spellings": { + "Engine": [ + "approval-engine/INTENT.md", + "audit-core/INTENT.md", + "flex-auth/INTENT.md", + "maturity-engine/INTENT.md", + "secrets-engine/INTENT.md", + "tenant-engine/INTENT.md", + "user-engine/INTENT.md", + "zone-engine/INTENT.md" + ], + "Staff": [ + "kings-guard/INTENT.md", + "ops-warden/INTENT.md" + ], + "engine": [ + "approval-engine/layer.yaml", + "audit-core/layer.yaml", + "maturity-engine/layer.yaml", + "secrets-engine/layer.yaml", + "tenant-engine/layer.yaml", + "user-engine/layer.yaml", + "zone-engine/layer.yaml" + ], + "staff": [ + "kings-guard/layer.yaml", + "ops-warden/layer.yaml" + ] + }, + "undeclared": [ + "gate-house", + "key-cape", + "net-kingdom", + "ops-mason" + ] +} diff --git a/internal/layer/conformance.go b/internal/layer/conformance.go index 42a561e..d8dbc74 100644 --- a/internal/layer/conformance.go +++ b/internal/layer/conformance.go @@ -11,7 +11,7 @@ import ( "gopkg.in/yaml.v3" ) -// Layer vocabulary from security-layer-model_v0.7 §3. +// Layer vocabulary from the Security Layer Model §3. var validLayers = map[string]bool{ "Staff": true, "Engine": true, @@ -80,8 +80,14 @@ func LoadDeclaration(path string) (Declaration, error) { if err != nil { return Declaration{}, err } + return parseDeclarationYAML(frontmatter) +} + +// parseDeclarationYAML unmarshals a bare declaration document. Shared with the +// estate survey, which reads layer.yaml files that carry no frontmatter fence. +func parseDeclarationYAML(doc string) (Declaration, error) { var decl Declaration - if err := yaml.Unmarshal([]byte(frontmatter), &decl); err != nil { + if err := yaml.Unmarshal([]byte(doc), &decl); err != nil { return Declaration{}, fmt.Errorf("parse layer declaration: %w", err) } return decl, nil diff --git a/internal/layer/survey.go b/internal/layer/survey.go new file mode 100644 index 0000000..648a93d --- /dev/null +++ b/internal/layer/survey.go @@ -0,0 +1,183 @@ +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 +} diff --git a/internal/layer/survey_test.go b/internal/layer/survey_test.go new file mode 100644 index 0000000..0e6b42f --- /dev/null +++ b/internal/layer/survey_test.go @@ -0,0 +1,89 @@ +package layer_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/netkingdom/flex-auth/internal/layer" +) + +func writeRepo(t *testing.T, root, name, intent, declFile string) { + t.Helper() + dir := filepath.Join(root, name) + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + if intent != "" { + body := "---\nlayer: " + intent + "\nrole: PDP\n---\n\n# x\n" + if err := os.WriteFile(filepath.Join(dir, "INTENT.md"), []byte(body), 0o644); err != nil { + t.Fatal(err) + } + } + if declFile != "" { + if err := os.WriteFile(filepath.Join(dir, "layer.yaml"), []byte("layer: "+declFile+"\n"), 0o644); err != nil { + t.Fatal(err) + } + } +} + +// The finding FLEX-WP-0030 B1 rests on: §11 accepts either form and does not +// say which governs when a repository carries both and they disagree. +func TestSurveyDetectsFormsDisagreeingWithinOneRepo(t *testing.T) { + root := t.TempDir() + writeRepo(t, root, "peer", "Engine", "engine") + + rows, err := layer.SurveyDeclarations(root, []string{"peer"}) + if err != nil { + t.Fatal(err) + } + if len(rows) != 1 { + t.Fatalf("rows = %d; want 1", len(rows)) + } + if !rows[0].SelfDisagrees() { + t.Fatal("Engine vs engine across the two §11 forms was not reported as disagreement") + } + if got := len(layer.SelfDisagreeing(rows)); got != 1 { + t.Fatalf("SelfDisagreeing = %d; want 1", got) + } +} + +// Case is not folded: whether §3 is case-insensitive is the open question, and +// folding here would hide the finding rather than resolve it. +func TestSurveyDoesNotFoldCase(t *testing.T) { + root := t.TempDir() + writeRepo(t, root, "peer", "", "engine") + + rows, _ := layer.SurveyDeclarations(root, []string{"peer"}) + if rows[0].File.InVocabulary { + t.Fatal(`"engine" was accepted into the §3 vocabulary; the survey must not fold case`) + } +} + +func TestSurveyReportsMissingDeclaration(t *testing.T) { + root := t.TempDir() + writeRepo(t, root, "silent", "", "") + + rows, _ := layer.SurveyDeclarations(root, []string{"silent"}) + if rows[0].Declared() { + t.Fatal("a repository with neither form was reported as declared") + } + if got := layer.Undeclared(rows); len(got) != 1 || got[0] != "silent" { + t.Fatalf("Undeclared = %v; want [silent]", got) + } +} + +// A single well-formed declaration must not be reported as disagreeing with +// itself — flex-auth is exactly this shape. +func TestSurveySingleFormIsNotDisagreement(t *testing.T) { + root := t.TempDir() + writeRepo(t, root, "solo", "Engine", "") + + rows, _ := layer.SurveyDeclarations(root, []string{"solo"}) + if rows[0].SelfDisagrees() { + t.Fatal("a repository with only INTENT.md was reported as self-disagreeing") + } + if !rows[0].Intent.InVocabulary { + t.Fatal(`"Engine" was rejected from the §3 vocabulary`) + } +} diff --git a/tools/survey_layer_declarations.go b/tools/survey_layer_declarations.go new file mode 100644 index 0000000..0eee627 --- /dev/null +++ b/tools/survey_layer_declarations.go @@ -0,0 +1,131 @@ +//go:build ignore + +// Command survey_layer_declarations runs the §11 mechanical check across the +// estate rather than against flex-auth alone. +// +// Why this exists: §11 calls the layer declaration mechanically checkable, but +// every checker in the estate reads only its own file. A check that cannot +// disagree with anyone has not been run. FLEX-WP-0030 B1 was found by an ad-hoc +// shell survey, which is the same defect in a different costume — a finding +// nobody can reproduce is an assertion. This makes it a command. +// +// It checks ONE property: whether each layer: value sits in the §3 vocabulary +// as written, case-sensitively. It does not apply flex-auth's own declaration +// rules to any other repository, and it does not grade anyone: §11 is explicit +// that a layer stated about a repository by another repository is not a +// declaration. +// +// go run tools/survey_layer_declarations.go --root ~ [--json out.json] +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + "os/user" + "path/filepath" + "sort" + "strings" + + "github.com/netkingdom/flex-auth/internal/layer" +) + +// Security-relevant counterparts from docs/conformance/boundaries-review.md. +var counterparts = []string{ + "approval-engine", "audit-core", "flex-auth", "gate-house", "key-cape", + "kings-guard", "maturity-engine", "net-kingdom", "ops-mason", "ops-warden", + "secrets-engine", "tenant-engine", "user-engine", "zone-engine", +} + +func main() { + root := flag.String("root", "", "directory holding the repositories (default: home)") + jsonOut := flag.String("json", "", "write a receipt to this path") + flag.Parse() + + dir := *root + if dir == "" { + u, err := user.Current() + if err != nil { + fail(err) + } + dir = u.HomeDir + } + + rows, err := layer.SurveyDeclarations(dir, counterparts) + if err != nil { + fail(err) + } + + fmt.Print(layer.FormatSurvey(rows)) + + spellings := layer.Spellings(rows) + undeclared := layer.Undeclared(rows) + + fmt.Printf("\n%d counterparts surveyed, %d declared, %d undeclared.\n", + len(rows), len(rows)-len(undeclared), len(undeclared)) + + keys := make([]string, 0, len(spellings)) + for k := range spellings { + keys = append(keys, k) + } + sort.Strings(keys) + + fmt.Printf("\nSpellings of layer: (%d distinct)\n", len(keys)) + for _, k := range keys { + fmt.Printf(" %-10s %s\n", k, strings.Join(spellings[k], ", ")) + } + + if len(undeclared) > 0 { + fmt.Printf("\nNo machine-readable declaration (§11 B2): %s\n", strings.Join(undeclared, ", ")) + } + + disagree := layer.SelfDisagreeing(rows) + if len(disagree) > 0 { + fmt.Printf("\nRepositories whose two §11 forms disagree (§11 B1): %d\n", len(disagree)) + for _, r := range disagree { + fmt.Printf(" %-18s INTENT.md=%-8q %s=%q\n", r.Repo, r.Intent.Layer, r.File.Source, r.File.Layer) + } + fmt.Println("\n§11 accepts \"a layer: key in INTENT.md frontmatter, OR an equivalent") + fmt.Println("declaration file\" and does not say which governs when both exist and") + fmt.Println("disagree. This is the open question, not a verdict.") + } + + var offVocab []string + for _, r := range rows { + for _, f := range []layer.Form{r.Intent, r.File} { + if f.Found && !f.InVocabulary { + offVocab = append(offVocab, fmt.Sprintf("%s=%q", f.Source, f.Layer)) + } + } + } + if len(offVocab) > 0 { + fmt.Printf("\nOutside the §3 vocabulary as written: %s\n", strings.Join(offVocab, ", ")) + } + + if *jsonOut != "" { + receipt := map[string]any{ + "derived_at": "run time", + "root": dir, + "rows": rows, + "spellings": spellings, + "undeclared": undeclared, + "off_vocab": offVocab, + "self_disagreeing": disagree, + "checks_only": "§3 vocabulary as written; no flex-auth house rules applied to peers", + } + b, err := json.MarshalIndent(receipt, "", " ") + if err != nil { + fail(err) + } + if err := os.WriteFile(filepath.Clean(*jsonOut), append(b, '\n'), 0o644); err != nil { + fail(err) + } + fmt.Printf("\nReceipt: %s\n", *jsonOut) + } +} + +func fail(err error) { + fmt.Fprintln(os.Stderr, "survey:", err) + os.Exit(1) +} diff --git a/workplans/FLEX-WP-0030-boundary-declaration-cleanup.md b/workplans/FLEX-WP-0030-boundary-declaration-cleanup.md index 0454317..3d2c7b6 100644 --- a/workplans/FLEX-WP-0030-boundary-declaration-cleanup.md +++ b/workplans/FLEX-WP-0030-boundary-declaration-cleanup.md @@ -153,23 +153,36 @@ state_hub_task_id: "70786e6d-02f2-5a90-8640-247ba377d9a4" Owner: `flex-auth` to raise; the named owner resolves each. -### B1 — `layer:` case is inconsistent, and §11 calls the check mechanical +### B1 — nine of nine repositories disagree with themselves across the two §11 forms -| Value written | Repositories | +**Corrected 2026-09-21.** The first version of this finding said the estate +spelled `layer:` three ways across repositories and named eight as non-conformant +on casing. That was wrong, from an ad-hoc shell survey that took the first +`^layer:` match per repository without recording which file it came from. + +Re-run mechanically (`tools/survey_layer_declarations.go`, receipt at +`docs/evidence/2026-09-21-layer-declaration-survey.json`): + +| Repositories carrying both §11 forms | 9 | | --- | --- | -| `Engine` | `flex-auth`, `maturity-engine` | -| `engine` | `secrets-engine`, `approval-engine`, `user-engine`, `tenant-engine`, `zone-engine`, `audit-core` | -| `staff` | `ops-warden`, `kings-guard` | +| Of those, forms that disagree | **9 of 9** | +| Shape | `INTENT.md`: `Engine` / `Staff`. `layer.yaml`: `engine` / `staff` | +| Only one form | `flex-auth` — no `layer.yaml`, which is why it cannot exhibit this | -flex-auth's own validator rejects anything outside `{Staff, Engine, Tooling}` -(`internal/layer/conformance.go:86`), so run across the estate it would fail -eight of ten declarations on casing alone. Either the §3 vocabulary is -case-insensitive and every checker MUST fold case, or it is not and eight -declarations are non-conformant. Today each repository's checker only reads its -own file, so the disagreement is invisible by construction. +The disagreement is **within** each repository, not across the estate, and it is +universal rather than careless: two generators, two conventions, no repository +mixing them. Nobody is inconsistent with anybody else. -Owner: `gate-house`. This is the §11 mechanical-checkability claim meeting its -first cross-repository run. +§11 accepts *"a `layer:` key in the `INTENT.md` frontmatter, **or** an equivalent +declaration file"* and does not say which governs when both exist and disagree. A +conformance run reading `INTENT.md` and one reading `layer.yaml` reach different +answers for nine repositories, and both follow §11. That is the finding: not +casing, but an unruled precedence between two permitted forms. + +Owner: `gate-house`. Two things would resolve it — which form governs, and +whether the §3 vocabulary is case-sensitive. If it is case-insensitive, the +casing half evaporates and flex-auth's validator is what changes; the precedence +half does not. ### B2 — four security-relevant repositories carry no layer declaration @@ -249,6 +262,11 @@ since no repository could have discovered it from inside its own checker. Task stays `progress`: every finding is open until its owner answers. None is closed by silence. +2026-09-21 (later): **B1 was sent wrong and corrected the same day.** See T05. +Corrections went to `gate-house` and the five engine repositories that received +the original, plus `ops-warden`, `kings-guard` and `audit-core`, who are affected +by the corrected finding and had not been told of the first. + ## Out of scope - Bumping flex-auth to declare v0.8. v0.8 is `status: proposed`; T01 removes the @@ -257,3 +275,39 @@ closed by silence. - Grading any peer repository's conformance. flex-auth reports what it can observe and names the owner; §9.3's two-owner split cuts here too. - `FLEX-WP-0029`'s stance-register second edition. Adjacent, separately owned. + +## 5. Make the survey reproducible + +```task +id: FLEX-WP-0030-T05 +status: done +priority: high +``` + +Owner: `flex-auth`. + +B1 was found with a shell pipeline and published as a finding. The review it +appeared in had already argued that a mechanical check nobody can re-run is an +assertion — and §11's whole claim is mechanical checkability. Asserting B1 +unmechanically was that defect committed by the repository reporting it. + +`tools/survey_layer_declarations.go` reads **both** §11 forms for every +security-relevant counterpart, reports intra-repository disagreement, and writes +a JSON receipt. `internal/layer` carries four tests covering the disagreement +case, the refusal to fold case, a missing declaration, and the single-form shape +that must not be reported as disagreeing with itself. + +It checks exactly one property — the `layer:` value against the §3 vocabulary as +written — and deliberately does **not** apply flex-auth's own declaration rules +(`pep_stance`, `tooling_contacts`, `conformance_record`) to any other repository. +Grading peers by the surveyor's house rules would be §11's own defect wearing a +tool for a hat. + +Running it immediately falsified the finding it was built to reproduce, which is +the argument for building it. + +Gate: B1 is reproducible by command, with a receipt, and the correction is +recorded rather than edited away. + +2026-09-21: done. Receipt at +`docs/evidence/2026-09-21-layer-declaration-survey.json`.