census-propose: Astra round 2 (two P2)
- Census messages are matched to changes by whole key, bounded by a space
before and a space, semicolon or end after: keys may contain spaces
("file.go:type T"), which word-splitting turned into "other" decisions.
- The overlay, card and manifest are written to temp files beside their
targets and renamed only once all three are on disk, manifest last.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
internal/provision/census_propose_internal_test.go+36 −0internal/provision/census_propose_test.go+52 −6
CI cancelled — superseded by a newer push — run details
CI log
=== gate === .amadan/test-gate.sh amadan: job cancelled — a newer push superseded it
Intent
<task-notification> <task-id>bzl175bpb</task-id> <tool-use-id>toolu_01QDwKtoPjRmo1u49YzW2wW6</tool-use-id> <output-file>/home/paulca/.cache/tmp/claude-1001/-home-paulca-amadan-net-carlos-platform/07894253-fbe8-42e3-8bd7-361304bbebdb/tasks/bzl175bpb.output</output-file> <status>completed</status> <summary>Background command "Run Astra round 2 review" completed (exit code 0)</summary> </task-notification>
Attributed to this commit: 16.9 g CO₂e (2103973 tokens). This is an attribution, not a measurement — usage counted from this commit's own marker forward to the next one on census-approvals, never backward from it. A turn that produced more than one commit books all of its tokens to the last one, and a concurrent session on this branch can add to the same figure. Some of it was priced using the shared default rate, because no model-specific factor row exists yet for at least one model involved. The Carbon view has the repo total, which is never derived from this attribution.
diff --git a/internal/provision/census_propose_internal_test.go b/internal/provision/census_propose_internal_test.go| index 979aea417..19a267956 100644 |
| --- a/internal/provision/census_propose_internal_test.go |
| +++ b/internal/provision/census_propose_internal_test.go |
| @@ -335,3 +335,39 @@ func TestCensusProposalRefusesWhatItDidNotWrite(t *testing.T) { |
| }) |
| } |
| } |
| + |
| +// Keys may contain spaces, and a directory is a prefix of its units' keys. |
| +func TestCensusNamesWholeKeys(t *testing.T) { |
| + for _, tc := range []struct { |
| + msg, key string |
| + want bool |
| + }{ |
| + {"changed root read internal/h/k.go:type gen; review class", "internal/h/k.go:type gen", true}, |
| + {"changed legacy declaration internal/h/k.go:type gen", "internal/h/k.go:type gen", true}, |
| + {"changed root read internal/h/k.go:type general; review", "internal/h/k.go:type gen", false}, |
| + {"changed root read internal/a/x.go:Foo; review", "internal/a", false}, |
| + {"changed root source envelope internal/a", "internal/a", true}, |
| + {"stale root read internal/a/x.go:Foo", "internal/a/x.go:Foo", true}, |
| + } { |
| + if got := censusNames(tc.msg, tc.key); got != tc.want { |
| + t.Errorf("censusNames(%q, %q) = %v, want %v", tc.msg, tc.key, got, tc.want) |
| + } |
| + } |
| +} |
| + |
| +// A body change to a unit whose key has a space is still an approval. |
| +func TestCensusClassifiesAKeyWithASpace(t *testing.T) { |
| + got, inv, seal := proposeFixture(t) |
| + key := "internal/a/t.go:type gen" |
| + e := classifiedRootRead{rootReadEntry: rootReadEntry{File: "internal/a/t.go", Unit: "type gen", Kinds: "registry:Host@3", Digest: strings.Repeat("7", 64)}, Class: "legacy", Task: "accepted-legacy", Basis: "accepted-legacy;rev3=J1;basis=root-route"} |
| + inv.Entries[key], inv.Frozen[key] = e, e |
| + inv.Legacy[rootReadLegacyFingerprint(e)] = true |
| + seal = rootReadLegacySeal(inv) |
| + got.Entries = append(got.Entries, e.rootReadEntry) |
| + got.Declarations[key] = e.Digest |
| + change(&got, key, "registry:Host@4", strings.Repeat("8", 64)) |
| + cs := classifyRootReads(got, inv, seal) |
| + if len(cs) != 1 || cs[0].Class != censusBody || cs[0].Key != key { |
| + t.Fatalf("changes = %+v, want one body for %q", cs, key) |
| + } |
| +} |
diff --git a/internal/provision/census_propose_test.go b/internal/provision/census_propose_test.go| index 904af347a..e4ee13494 100644 |
| --- a/internal/provision/census_propose_test.go |
| +++ b/internal/provision/census_propose_test.go |
| @@ -178,8 +178,8 @@ func classifyRootReads(got rootReadScan, inv rootReadInventory, seal string) []c |
| } |
| for _, msg := range censusDiscrepancies(got, inv, seal) { |
| explained := false |
| - for _, tok := range strings.Fields(msg) { |
| - if keys[strings.TrimRight(tok, ";,")] { |
| + for key := range keys { |
| + if censusNames(msg, key) { |
| explained = true |
| break |
| } |
| @@ -197,6 +197,24 @@ func classifyRootReads(got rootReadScan, inv rootReadInventory, seal string) []c |
| 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] |
| @@ -509,12 +527,40 @@ func writeRootReadProposal(root, name string, p rootReadProposal) error { |
| } |
| lines = append(lines, file+"\t"+p.Seal) |
| note := filepath.Join(root, "docs/superpowers/notes", "census-proposal-"+name+".md") |
| - for _, w := range []struct{ path, text string }{ |
| + // 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). |
| + writes := []struct{ path, text string }{ |
| {overlay, p.Overlay}, |
| - {manifest, strings.Join(lines, "\n") + "\n"}, |
| {note, p.Card}, |
| - } { |
| - if err := os.WriteFile(w.path, []byte(w.text), 0o644); err != nil { |
| + {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 |
| } |
| } |