package provision // census-propose (#369): census rulings as approvals. The root-read census // (TestPlan14RootReadCensus) fails on any change to a unit it tracks or to // a package envelope. Most of those changes are approvals, not decisions: // the same reads in an edited body, or an envelope fingerprint that moved. // This file classifies each discrepancy, writes the overlay that closes the // approvable ones (rows, @legacy fingerprints, envelope re-pins, the seal, // the chain manifest line), and the approval card an independent reviewer // reads to approve it. Changes that need a disposition (a new unit, a removed one, // the scanner's field list) are decisions: it names them and writes // nothing. // // make census-card print the card; writes nothing // make census-propose NAME= CLAIMS= write the overlay and the card // // CLAIMS has one line per changed unit, ":\t". // Rows carry rev3=census-ruling- from the start: approval happens // before the merge, so there is no PROPOSED row to flip afterwards. import ( "fmt" "os" "path/filepath" "regexp" "sort" "strings" "testing" ) // Change classes. The first three are approvals; the rest are decisions. const ( censusBody = "body" // digest moved, the same reads (offsets aside) censusSites = "sites" // more or fewer reads of kinds the unit already makes censusEnvelope = "envelope" // a package's source fingerprint moved censusReads = "reads" // a kind of read the unit did not make before censusNew = "new" // an unclassified unit: needs a disposition censusRemoved = "removed" // a unit or envelope gone: needs a removal row censusOther = "other" // a census discrepancy no change above explains ) // censusDecision: a new kind of read can make a unit's disposition wrong // (a registry reader that starts opening files is not the unit that was // classified), so it is the owner's call, as a new or removed unit is // (Astra review, census-approvals P1). func censusDecision(class string) bool { return class == censusReads || class == censusNew || class == censusRemoved || class == censusOther } type censusChange struct { Key, Class string Before classifiedRootRead // the inventory's row; zero for a new unit After rootReadEntry // the scan's; zero for a removed unit // An envelope's fingerprints. OldHash, NewHash string } // kindCounts is a unit's read sites without their offsets. func kindCounts(kinds string) map[string]int { m := map[string]int{} if kinds == "" { return m } for _, k := range strings.Split(kinds, ",") { name, _, _ := strings.Cut(k, "@") m[name]++ } return m } // kindDiff says how a unit's read sites changed: "+registry:Socket", // "-file:Open ×2", or "unchanged (N)". func kindDiff(before, after string) string { b, a := kindCounts(before), kindCounts(after) names := map[string]bool{} for k := range b { names[k] = true } for k := range a { names[k] = true } var sorted []string for k := range names { sorted = append(sorted, k) } sort.Strings(sorted) var parts []string for _, k := range sorted { d := a[k] - b[k] sign := "+" if d < 0 { sign, d = "-", -d } switch { case d == 1: parts = append(parts, sign+k) case d > 1: parts = append(parts, fmt.Sprintf("%s%s ×%d", sign, k, d)) } } if len(parts) == 0 { n := 0 for _, c := range a { n += c } return fmt.Sprintf("unchanged (%d)", n) } return strings.Join(parts, ", ") } func sameKinds(before, after string) bool { return kindDiff(before, after) == kindDiff(after, after) } // newKinds reports whether after reads a kind before did not. func newKinds(before, after string) bool { b := kindCounts(before) for k := range kindCounts(after) { if b[k] == 0 { return true } } return false } // classifyRootReads names every discrepancy the census would report under // seal, one change per unit or envelope. A discrepancy no unit or envelope // change accounts for (a removed declaration back in the tree, the seal // itself, the scanner's field list) is an "other" decision carrying the // census's own message, so the card never shows less than the census does // (Astra review, census-approvals P2). func classifyRootReads(got rootReadScan, inv rootReadInventory, seal string) []censusChange { var cs []censusChange seen := map[string]bool{} for _, g := range got.Entries { key := g.File + ":" + g.Unit seen[key] = true w, ok := inv.Entries[key] switch { case !ok: cs = append(cs, censusChange{Key: key, Class: censusNew, After: g}) case w.Kinds == g.Kinds && w.Digest == g.Digest && declarationHolds(got, inv, key): case sameKinds(w.Kinds, g.Kinds): cs = append(cs, censusChange{Key: key, Class: censusBody, Before: w, After: g}) case newKinds(w.Kinds, g.Kinds): cs = append(cs, censusChange{Key: key, Class: censusReads, Before: w, After: g}) default: cs = append(cs, censusChange{Key: key, Class: censusSites, Before: w, After: g}) } } for key, w := range inv.Entries { if seen[key] { continue } decl, declared := got.Declarations[key] switch { case w.Class != "legacy" && w.Kinds == "" && declared && decl == w.Digest: // Declared, reads nothing, unchanged: not a discrepancy. case w.Class != "legacy" && w.Kinds == "" && declared: cs = append(cs, censusChange{Key: key, Class: censusBody, Before: w, After: rootReadEntry{File: w.File, Unit: w.Unit, Digest: decl}}) default: cs = append(cs, censusChange{Key: key, Class: censusRemoved, Before: w}) } } for dir, hash := range got.Coverage { if inv.Coverage[dir] != hash { cs = append(cs, censusChange{Key: dir, Class: censusEnvelope, OldHash: inv.Coverage[dir], NewHash: hash}) } } for dir, hash := range inv.Coverage { if _, ok := got.Coverage[dir]; !ok { cs = append(cs, censusChange{Key: dir, Class: censusRemoved, OldHash: hash}) } } keys := map[string]bool{} for _, c := range cs { keys[c.Key] = true } for _, msg := range censusDiscrepancies(got, inv, seal) { explained := false for key := range keys { if censusNames(msg, key) { explained = true break } } if !explained { cs = append(cs, censusChange{Key: msg, Class: censusOther}) } } sort.Slice(cs, func(i, j int) bool { if cs[i].Class != cs[j].Class { return cs[i].Class < cs[j].Class } return cs[i].Key < cs[j].Key }) return cs } // censusNames reports whether a census message names key whole: keys may // contain spaces ("file.go:type T"), and an envelope's directory is a // prefix of its units' keys, so the key must be bounded by a space before // it and a space, a semicolon or the end after it (Astra round 2). func censusNames(msg, key string) bool { for i := 0; ; { j := strings.Index(msg[i:], " "+key) if j < 0 { return false } end := i + j + 1 + len(key) if end == len(msg) || msg[end] == ' ' || msg[end] == ';' { return true } i += j + 1 } } // declarationHolds is checkRootReadDeclarations's test for one unit. func declarationHolds(got rootReadScan, inv rootReadInventory, key string) bool { frozen, ok := inv.Frozen[key] if !ok { return true } cur := inv.Entries[key] if cur.Class == "legacy" { return got.Declarations[key] == frozen.Digest && rootReadLegacyFingerprint(cur) == rootReadLegacyFingerprint(frozen) } return got.Declarations[key] == cur.Digest } type rootReadProposal struct { Overlay, Seal, Card string } var censusName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`) // proposeRootReads writes the overlay that closes every change in the scan, // re-freezing each changed unit at the disposition it already has with its // claim appended, under census-ruling-. It refuses decisions, a // change with no claim and a claim for no change, and checks its own work: // loaded onto a copy of inv, the overlay must leave the census clean. func proposeRootReads(got rootReadScan, inv rootReadInventory, seal, name string, claims map[string]string) (rootReadProposal, error) { if !censusName.MatchString(name) { return rootReadProposal{}, fmt.Errorf("census name %q: want lower-case letters, digits and dashes", name) } cs := classifyRootReads(got, inv, seal) if len(cs) == 0 { return rootReadProposal{}, fmt.Errorf("nothing to propose: the census is clean") } var decisions []string units := map[string]bool{} for _, c := range cs { if censusDecision(c.Class) { decisions = append(decisions, c.Class+" "+c.Key) } if c.Class == censusBody || c.Class == censusSites { units[c.Key] = true } } if len(decisions) > 0 { return rootReadProposal{}, fmt.Errorf("a decision is needed, not an approval: %s; write those rows by hand and ask on #280", strings.Join(decisions, "; ")) } for key := range units { if strings.TrimSpace(claims[key]) == "" { return rootReadProposal{}, fmt.Errorf("no claim for %s: say what changed in the CLAIMS file", key) } } for key, claim := range claims { if !units[key] { return rootReadProposal{}, fmt.Errorf("claim for %s: not a change the census sees", key) } if strings.ContainsAny(claim, "\t\n\r") { return rootReadProposal{}, fmt.Errorf("claim for %s: a tab or newline would break the row", key) } } ruling := "census-ruling-" + name var frozen, plain, cover []string counts := map[string]int{} for _, c := range cs { counts[c.Class]++ if c.Class == censusEnvelope { cover = append(cover, "@coverage\t"+c.Key+"\t"+c.NewHash) continue } e := classifiedRootRead{rootReadEntry: c.After, Class: c.Before.Class, Task: c.Before.Task, Basis: c.Before.Basis + ";rev3=" + ruling + ";basis=" + strings.TrimSpace(claims[c.Key])} row := strings.Join([]string{e.File, e.Unit, e.Class, e.Task, e.Basis, e.Kinds, e.Digest}, "\t") if e.Class == "legacy" { frozen = append(frozen, "@legacy\t"+rootReadLegacyFingerprint(e)+"\t"+row) } plain = append(plain, row) } header := []string{ fmt.Sprintf("# Census overlay %s, "+censusProposeMark+". %s, approved on #280 before the merge; the approval card is docs/superpowers/notes/census-proposal-%s.md.", name, ruling, name), fmt.Sprintf("# %d body, %d sites, %d envelope: each changed unit re-frozen at its existing disposition with its claim appended.", counts[censusBody], counts[censusSites], counts[censusEnvelope]), } overlay := strings.Join(append(append(append(header, frozen...), plain...), cover...), "\n") + "\n" after := cloneRootReadInventory(inv) f, err := os.CreateTemp("", "census-overlay-*.tsv") if err != nil { return rootReadProposal{}, err } defer os.Remove(f.Name()) if _, err := f.WriteString(overlay); err != nil { f.Close() return rootReadProposal{}, err } if err := f.Close(); err != nil { return rootReadProposal{}, err } if err := loadRootReadInventory(f.Name(), &after, true); err != nil { return rootReadProposal{}, fmt.Errorf("the overlay does not load: %w", err) } newSeal := rootReadLegacySeal(after) if errs := censusDiscrepancies(got, after, newSeal); len(errs) != 0 { return rootReadProposal{}, fmt.Errorf("the overlay leaves discrepancies: %s", strings.Join(errs, "; ")) } return rootReadProposal{Overlay: overlay, Seal: newSeal, Card: rootReadCard(name, cs, claims, seal, newSeal)}, nil } func censusDiscrepancies(got rootReadScan, inv rootReadInventory, seal string) []string { errs := append(compareRootReads(got, inv), checkRootReadLegacy(inv, seal)...) return append(errs, checkRootReadDeclarations(got, inv)...) } // rootReadCard is what the reviewer reads to approve the change (AGENTS.md, // The census): someone other than the author, because the classes count // kinds of read, not where a read points. newSeal is empty for a dry run // (make census-card). func rootReadCard(name string, cs []censusChange, claims map[string]string, seal, newSeal string) string { var b strings.Builder fmt.Fprintf(&b, "# Census approval: %s\n\n", name) var decisions []censusChange var envelopes []string var units []censusChange for _, c := range cs { switch { case censusDecision(c.Class): decisions = append(decisions, c) case c.Class == censusEnvelope: envelopes = append(envelopes, "`"+c.Key+"`") default: units = append(units, c) } } switch { case len(cs) == 0: fmt.Fprintf(&b, "Nothing to approve: the census is clean (seal `%s`).\n", seal) return b.String() case len(decisions) == 0: fmt.Fprintf(&b, "Ruling `census-ruling-%s`. Every change below keeps the disposition it already has, so it needs an approval, not a decision.\n\n", name) default: fmt.Fprintf(&b, "Ruling `census-ruling-%s`. %d change(s) need a decision (below), which census-propose cannot write.\n\n", name, len(decisions)) } if len(units) > 0 { b.WriteString("| unit | change | read sites | what changed |\n|---|---|---|---|\n") for _, c := range units { claim := strings.TrimSpace(claims[c.Key]) if claim == "" { claim = "(no claim yet)" } fmt.Fprintf(&b, "| `%s` `%s` | %s | %s | %s |\n", c.After.File, c.After.Unit, c.Class, kindDiff(c.Before.Kinds, c.After.Kinds), claim) } b.WriteString("\n") } if len(envelopes) > 0 { fmt.Fprintf(&b, "Envelopes re-pinned: %s.\n\n", strings.Join(envelopes, ", ")) } if len(units) > 0 || len(envelopes) > 0 { b.WriteString(censusReviewQuestion) } if len(units) > 0 { b.WriteString("Recorded dispositions, which each change must still satisfy:\n\n") for _, c := range units { fmt.Fprintf(&b, "- `%s` `%s`: %s, task %s. Basis: %s\n", c.Before.File, c.Before.Unit, c.Before.Class, c.Before.Task, c.Before.Basis) } b.WriteString("\n") } if len(decisions) > 0 { b.WriteString("Decisions (write their rows by hand and ask on #280):\n\n") for _, c := range decisions { detail := "" switch c.Class { case censusNew: detail = ": reads " + kindDiff("", c.After.Kinds) case censusReads: detail = ": " + kindDiff(c.Before.Kinds, c.After.Kinds) } fmt.Fprintf(&b, "- %s `%s`%s\n", c.Class, c.Key, detail) } b.WriteString("\n") } if newSeal != "" { fmt.Fprintf(&b, "Seal: `%s` → `%s`.\n\nOverlay: `internal/provision/testdata/%s`, appended to `%s`.\n", seal, newSeal, rootReadOverlayFile(name), rootReadChainManifest) } else { fmt.Fprintf(&b, "Seal now: `%s`. Dry run: nothing written.\n", seal) } return b.String() } // censusReviewQuestion is what the independent reviewer answers for the // approvals on a card (AGENTS.md, The census). const censusReviewQuestion = "Review: a model from the other family than the author reads the branch's diff against main on the amadan.net remote (`origin` or `amadan`, whichever points at amadan.net) and gives a verdict for every unit and every re-pinned envelope above. For a unit, compare the change with its recorded disposition and basis below; for an envelope, read every changed function in the package and what it reaches. Escalate if the change lets root open, run, query or follow anything it did not before from a path or input a tenant, an app or a box agent can create, write or name; if it weakens a check, refusal, validation, argument or environment the basis relies on; or if the diff cannot settle either. Otherwise approve. Record each verdict on #280 with the tip. An escalation goes to the census owner as a decision.\n\n" // censusProposeMark is in the first line of every overlay census-propose // writes: a re-run replaces only a file that carries it. const censusProposeMark = "written by census-propose (#369)" func rootReadOverlayFile(name string) string { return "plan14-root-reads-" + name + ".tsv" } func cloneRootReadInventory(inv rootReadInventory) rootReadInventory { out := rootReadInventory{Fields: inv.Fields} out.Entries = cloneMap(inv.Entries) out.Frozen = cloneMap(inv.Frozen) out.Removed = cloneMap(inv.Removed) out.Coverage = cloneMap(inv.Coverage) out.Legacy = cloneMap(inv.Legacy) return out } func cloneMap[V any](m map[string]V) map[string]V { if m == nil { return nil } out := make(map[string]V, len(m)) for k, v := range m { out[k] = v } return out } // readCensusClaims reads a CLAIMS file: ":\t" // per line, blank lines and # comments ignored. func readCensusClaims(path string) (map[string]string, error) { raw, err := os.ReadFile(path) if err != nil { return nil, err } claims := map[string]string{} for i, line := range strings.Split(string(raw), "\n") { line = strings.TrimRight(line, "\r") if strings.TrimSpace(line) == "" || strings.HasPrefix(line, "#") { continue } key, claim, ok := strings.Cut(line, "\t") if !ok || !strings.Contains(key, ":") || strings.TrimSpace(claim) == "" { return nil, fmt.Errorf("%s:%d: want :", path, i+1) } if _, dup := claims[key]; dup { return nil, fmt.Errorf("%s:%d: a second claim for %s", path, i+1, key) } claims[key] = claim } return claims, nil } const ( censusCardStart = "--- census card ---" censusCardEnd = "--- end census card ---" ) // TestPlan14RootReadCensusPropose is census-propose and census-card (see // the Makefile). It does nothing unless one of them asked. func TestPlan14RootReadCensusPropose(t *testing.T) { name, dry := os.Getenv("CARLOS_CENSUS_PROPOSE"), os.Getenv("CARLOS_CENSUS_CARD") != "" if name == "" && !dry { t.Skip("run by make census-card and make census-propose") } root := censusRoot(t) // A re-run (the code changed again) proposes against the chain before // its own overlay, which must still be the manifest's last; nothing is // changed on disk until the new proposal is built and checked. before := "" if !dry { if !censusName.MatchString(name) { t.Fatalf("census name %q: want lower-case letters, digits and dashes", name) } before = rootReadOverlayFile(name) } got, err := rootReadProduction(root) if err != nil { t.Fatal(err) } inv, seal := rootReadChainBefore(t, root, before) claims := map[string]string{} if path := os.Getenv("CARLOS_CENSUS_CLAIMS"); path != "" { if claims, err = readCensusClaims(path); err != nil { t.Fatal(err) } } if dry { if name == "" { name = "" } fmt.Printf("%s\n%s%s\n", censusCardStart, rootReadCard(name, classifyRootReads(got, inv, seal), claims, seal, ""), censusCardEnd) return } p, err := proposeRootReads(got, inv, seal, name, claims) if err != nil { t.Fatal(err) } if err := writeRootReadProposal(root, name, p); err != nil { t.Fatal(err) } fmt.Printf("%s\n%s%s\n", censusCardStart, p.Card, censusCardEnd) } // writeRootReadProposal writes a checked proposal: the overlay, its line // at the end of the chain manifest (replacing its own line on a re-run) // and the approval card. It refuses, writing nothing, to replace an // overlay census-propose did not write, or one that is not the manifest's // last. func writeRootReadProposal(root, name string, p rootReadProposal) error { file := rootReadOverlayFile(name) overlay := filepath.Join(root, "internal/provision/testdata", file) if raw, err := os.ReadFile(overlay); err == nil { first, _, _ := strings.Cut(string(raw), "\n") if !strings.Contains(first, censusProposeMark) { return fmt.Errorf("%s exists and census-propose did not write it: propose under a new name", file) } } else if !os.IsNotExist(err) { return err } manifest := filepath.Join(root, rootReadChainManifest) raw, err := os.ReadFile(manifest) if err != nil { return err } lines := strings.Split(strings.TrimRight(string(raw), "\n"), "\n") links, err := readRootReadChainManifest(manifest) if err != nil { return err } for i, l := range links { if l.File != file { continue } if i != len(links)-1 || lines[len(lines)-1] != l.File+"\t"+l.Seal { return fmt.Errorf("%s is not the last overlay in %s: propose under a new name", file, rootReadChainManifest) } lines = lines[:len(lines)-1] } lines = append(lines, file+"\t"+p.Seal) note := filepath.Join(root, "docs/superpowers/notes", "census-proposal-"+name+".md") // Every file is written beside its target first and renamed only once // all three are on disk, the manifest last, so a failed write leaves // the previous proposal whole (Astra round 2). A rename failing between // the others is not rolled back (Astra round 3, declined): the files // are a working tree's, the census fails on any mismatch, and a re-run // recovers, since the manifest still ends in this name's line. writes := []struct{ path, text string }{ {overlay, p.Overlay}, {note, p.Card}, {manifest, strings.Join(lines, "\n") + "\n"}, } var temps []string defer func() { for _, tmp := range temps { os.Remove(tmp) } }() for _, w := range writes { f, err := os.CreateTemp(filepath.Dir(w.path), "."+filepath.Base(w.path)+".*") if err != nil { return err } temps = append(temps, f.Name()) if _, err := f.WriteString(w.text); err != nil { f.Close() return err } if err := f.Chmod(0o644); err != nil { f.Close() return err } if err := f.Close(); err != nil { return err } } for i, w := range writes { if err := os.Rename(temps[i], w.path); err != nil { return err } } return nil }