v0.297.0: burn-down round 2 — 24 small rows (R-591 R-568 R-567 R-363 R-547 R-10 R-552 R-251 R-104 R-619 R-362 R-675 R-256 R-257 R-240 R-365 R-425 R-565 R-564 R-603 R-454 R-208 R-457-swept) + the banner countdown and deepCopyStack twins; MinAgent 0.131.0
gates / gates (push) Failing after 50s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-05 20:12:56 +02:00
parent 114ff2761a
commit 1453cfc69b
68 changed files with 3953 additions and 116 deletions
@@ -0,0 +1,143 @@
package i18n
import (
"encoding/json"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"testing"
)
// R-603: html/template escapes ' " & < > in DATA. A bundle value that Go code renders into a page as
// data (a flash, a note, an error) therefore never appears verbatim on the page: "The system backup's
// drive…" arrives as "backup&#39;s", and a strings.Contains assertion for it fails exactly like a
// missing sentence — which sends the next person to re-fix a handler that was never broken. (Template
// copy is expanded textually by Expand and is NOT escaped; only Go-named values travel as data.)
//
// THE GATE: a Go-named value may not carry an HTML-escapable character unless it is registered below.
// The registered set is the measured state on 2026-10-05; a NEW value fails here with this pointer,
// so whoever writes it either rewords it or registers it AND asserts it with html.EscapeString(want).
var r603Registered = map[string]bool{
"alert.endpoint_drift": true, "kept.choice.title": true, "kept.choice.fresh.desc": true,
"err.backup.unit_version_mismatch": true, "note.tier2.unit_preserved": true,
"err.stacks.setup_gate_failed": true, "err.kept.occupied": true, "update.refusal.no_backup": true,
"update.error.journal_failed": true, "note.unit_restore_settings_only": true,
"note.tier2_no_coverage": true, "note.tier2_unit_available": true, "note.tier2_unit_not_covered": true,
"note.tier2_unit_confirm_base": true, "note.tier2_unit_stale_clause": true,
"note.tier2_unit_stale_notice_fmt": true, "note.restore.whole_files": true,
"note.restore.scratch_state": true,
}
var r603Escapable = regexp.MustCompile(`['"&<>]`)
var r603KeyShape = regexp.MustCompile(`^[a-z][a-z0-9_]*(?:\.[a-z0-9_\-]+)+$`)
// goNamedKeys returns every bundle key that appears as a string literal in non-test Go source under
// the given roots — the same "named in Go" rule scripts/i18n_go_parity.py applies.
func goNamedKeys(t *testing.T, bundle map[string]string, roots ...string) map[string]string {
t.Helper()
out := map[string]string{}
fset := token.NewFileSet()
for _, root := range roots {
err := filepath.Walk(root, func(p string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || !strings.HasSuffix(p, ".go") || strings.HasSuffix(p, "_test.go") {
return err
}
f, perr := parser.ParseFile(fset, p, nil, 0)
if perr != nil {
return perr
}
ast.Inspect(f, func(n ast.Node) bool {
if lit, ok := n.(*ast.BasicLit); ok && lit.Kind == token.STRING {
if v, uerr := strconv.Unquote(lit.Value); uerr == nil && r603KeyShape.MatchString(v) {
if _, ok := bundle[v]; ok {
out[v] = p
}
}
}
return true
})
return nil
})
if err != nil {
t.Fatal(err)
}
}
return out
}
func r603Load(t *testing.T, lang string) map[string]string {
t.Helper()
raw, err := os.ReadFile(filepath.Join("locales", lang+".json"))
if err != nil {
t.Fatal(err)
}
var generic map[string]interface{}
if err := json.Unmarshal(raw, &generic); err != nil {
t.Fatal(err)
}
out := map[string]string{}
for k, v := range generic {
if s, ok := v.(string); ok {
out[k] = s
}
}
return out
}
// r603Offenders is the gate's verdict for one bundle: Go-named values carrying an escapable character
// that are not registered.
func r603Offenders(bundle, named map[string]string) []string {
var bad []string
for k := range named {
if r603Escapable.MatchString(bundle[k]) && !r603Registered[k] {
bad = append(bad, k)
}
}
sort.Strings(bad)
return bad
}
func TestR603_GoNamedValuesDoNotHideBehindHTMLEscaping(t *testing.T) {
hu, en := r603Load(t, "hu"), r603Load(t, "en")
named := goNamedKeys(t, hu, filepath.Join("..", "..", "internal"), filepath.Join("..", "..", "cmd"))
if len(named) < 100 {
t.Fatalf("control: only %d Go-named keys found — the source walk is not reaching the code", len(named))
}
// Decoy: a planted apostrophe in a Go-named value that is not registered must be caught.
var some string
for k := range named {
if !r603Registered[k] {
some = k
break
}
}
planted := map[string]string{}
for k, v := range en {
planted[k] = v
}
planted[some] = "The drive's copy"
if got := r603Offenders(planted, named); len(got) != 1 || got[0] != some {
t.Fatalf("control: a planted apostrophe in %s was not caught (got %v)", some, got)
}
for lang, b := range map[string]map[string]string{"hu": hu, "en": en} {
for _, k := range r603Offenders(b, named) {
t.Errorf("R-603 [%s] %s = %q (named in %s) carries a character html/template escapes in data "+
"(' \" & < >): on the page it is not these bytes, and a strings.Contains assertion for it fails "+
"like a missing sentence. Reword it (a typographic ’ is not escaped), or register it in "+
"r603Registered and assert it with html.EscapeString(want).", lang, k, b[k], named[k])
}
}
// A registration that no longer needs to be there is reported, so the list only shrinks.
for k := range r603Registered {
if _, ok := named[k]; !ok || !r603Escapable.MatchString(en[k]+hu[k]) {
t.Errorf("R-603: %s is registered but no longer a Go-named value with an escapable character — remove it", k)
}
}
}