R-576: go-parity gate also judges the CALL — arity vs printf verbs, and no key glued with +

Checks 4 (ARITY) and 5 (NO-CONCAT) in i18n_go_parity.py: at every message-helper call
(MsgError/MsgErrorf/Text/Msgf/msg/msgLang/msgHU/note) whose key is a literal hu.json knows,
the argument count must equal the Hungarian value's printf verbs; a known key literal may
never be an operand of +. 485 calls judged, 0 defects today. Decoys: a dropped argument and
a key+suffix concatenation on a real producer convict; a nested-paren argument is accepted.

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 21:54:41 +02:00
parent 7b8216abac
commit 074ddda352
2 changed files with 178 additions and 4 deletions
+161 -3
View File
@@ -33,6 +33,12 @@ Checks:
3. BYTE-EQUAL. A single-literal key: hu.json[key] == from, exactly. A joined key: hu.json[key]
with its printf verbs removed == the `from` fragments concatenated in order, with their verbs
removed too. Plural keys compare their `.one`/`.other`-less base form.
4. ARITY (R-576). At a message-helper call whose key is a literal hu.json knows, the arguments
after the key are as many as the Hungarian value's printf verbs.
5. NO-CONCAT (R-576). A literal naming a bundle key is never an operand of `+`.
Checks 1-3 ask whether the TEXT is real; 4-5 ask whether the CALL kept all of it. A structural
gate over the text cannot see a defect in the call -- that is how 7 producers lost half their
sentence on 2026-09-18 with this gate green.
The Go literal walker is the inventory's (felhom.eu/scripts/i18n_inventory.py `go_literals`),
copied rather than imported: this gate runs from a controller clone that may not sit beside
@@ -233,6 +239,150 @@ def base_form(hu: dict, key: str):
return None
# ---- R-576: the CALL, not only the TEXT ---------------------------------------------------------
#
# Checks 1-3 ask "is the text a key carries real?". They cannot see a call site that LOST text: on
# 2026-09-18 the bulk converter turned `fmt.Errorf("a: "+ "b: %s", x)` into a call that kept only
# the key and dropped the continuation and its argument -- 7 producers, and this gate stayed GREEN,
# because every surviving fragment WAS a byte-equal base literal. Two more questions convict it:
#
# 4. ARITY. At a message-helper call whose key argument is a literal hu.json knows, the number of
# arguments after the key equals the number of printf verbs in the Hungarian value. A call that
# spreads a slice (`args...`) is not counted -- its arity is not visible in the source.
# 5. NO-CONCAT. A literal that names a bundle key is never an operand of `+`. A key is a whole
# name; a key glued to more text is either a half-converted concatenation or a key nobody can
# find.
#
# helper name -> index of its KEY argument. Matched by NAME (no type checker), so only a call whose
# key argument is a literal hu.json actually knows is ever judged.
MSG_HELPERS = {
"MsgError": 0, # util.MsgError(key, args...)
"MsgErrorf": 1, # util.MsgErrorf(kind, key, args...)
"Text": 1, # util.Text(lang, key, args...)
"Msgf": 1, # (*i18n.Bundle).Msgf(lang, key, args...)
"msg": 1, # (*Router|*Server).msg(req, key, args...)
"msgLang": 1, # (*Router|*Server).msgLang(lang, key, args...)
"msgHU": 0, # stacks.msgHU(key, args...)
"note": 0, # (*backup.Manager).note(key, args...)
}
CALL_RE = re.compile(r"\b(" + "|".join(sorted(MSG_HELPERS, key=len, reverse=True)) + r")\(")
def code_mask(src: str):
"""(mask, lits): mask is 1 where src[i] is CODE (outside comments, strings and runes); lits is
the (start, end) span of every interpreted string literal, quotes included."""
mask = bytearray(len(src))
lits = []
i, n = 0, len(src)
while i < n:
c = src[i]
if src.startswith("//", i):
j = src.find("\n", i)
i = n if j < 0 else j
elif src.startswith("/*", i):
j = src.find("*/", i + 2)
i = n if j < 0 else j + 2
elif c in "'\"":
j = i + 1
while j < n and src[j] != c and src[j] != "\n":
j += 2 if src[j] == "\\" else 1
if c == '"':
lits.append((i, j + 1))
i = j + 1
elif c == "`":
j = src.find("`", i + 1)
i = n if j < 0 else j + 1
else:
mask[i] = 1
i += 1
return mask, lits
def split_args(src: str, mask, open_paren: int):
"""The top-level argument source strings of the call whose '(' is at open_paren, or None."""
depth, start, args, i, n = 0, open_paren + 1, [], open_paren, len(src)
while i < n:
if mask[i]:
c = src[i]
if c in "([{":
depth += 1
elif c in ")]}":
depth -= 1
if depth == 0:
tail = src[start:i].strip()
if tail:
args.append(tail)
return args
elif c == "," and depth == 1:
args.append(src[start:i].strip())
start = i + 1
i += 1
return None
def verb_arity(value: str) -> int:
"""How many arguments the printf verbs of value consume (explicit indexes respected)."""
s = value.replace("%%", "")
need, nxt = 0, 0
for m in VERB_RE.finditer(s):
idx = re.match(r"%\[(\d+)\]", m.group(0))
if idx:
nxt = int(idx.group(1))
else:
nxt += 1
need = max(need, nxt)
return need
STR_LIT_RE = re.compile(r'^"((?:[^"\\\n]|\\.)*)"$')
def call_site_defects(root: str, hu) -> tuple:
"""Checks 4 (ARITY) and 5 (NO-CONCAT) over every in-scope Go file -> (defects, calls judged)."""
bad, checked = [], 0
if hu is None:
return bad, checked
for p in go_files(root):
rel = os.path.relpath(p, root)
src = read(p)
mask, lits = code_mask(src)
# 5. NO-CONCAT: a known key literal with a `+` on either side.
for a, b in lits:
text = unescape(src[a + 1:b - 1])
if text not in hu or not KEY_SHAPE_RE.match(text):
continue
left = src[:a].rstrip(" \t")
right = src[b:].lstrip(" \t")
if left.endswith("+") or right.startswith("+"):
line = src.count("\n", 0, a) + 1
bad.append("CONCAT %s is glued to more text with + at %s:%d -- a key is a whole "
"name; this is a half-converted concatenation" % (text, rel, line))
# 4. ARITY.
for m in CALL_RE.finditer(src):
if not mask[m.start()]:
continue
args = split_args(src, mask, m.end() - 1)
k = MSG_HELPERS[m.group(1)]
if args is None or len(args) <= k:
continue
lit = STR_LIT_RE.match(args[k])
if not lit:
continue
key = unescape(lit.group(1))
if key not in hu or not KEY_SHAPE_RE.match(key):
continue # a plural key (.one/.other only) or not a key at all
rest = args[k + 1:]
if rest and rest[-1].endswith("..."):
continue # a spread slice: arity not visible
checked += 1
want = verb_arity(hu[key])
if len(rest) != want:
line = src.count("\n", 0, m.start()) + 1
bad.append("ARITY %s called with %d argument(s) at %s:%d, its Hungarian value "
"has %d printf verb(s): %r" % (key, len(rest), rel, line, want, hu[key]))
return bad, checked
def main(argv) -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--capture", action="store_true",
@@ -334,16 +484,24 @@ def main(argv) -> int:
" base (verbs removed): %r"
% (key, len(froms), got, want))
# 4 + 5. ARITY and NO-CONCAT (R-576) -- the call, not the text.
call_bad, judged = call_site_defects(CTRL, hu)
bad.extend(call_bad)
if judged == 0:
bad.append("NO-CALLS no message-helper call with a literal key was found -- the call scanner "
"no longer matches the code, so checks 4-5 would be green by seeing nothing")
print("go-parity: base capture %s (%d literals), %d slice-2 keys listed, %d pre-existing, "
"%d named in Go"
"%d named in Go, %d helper calls judged for arity"
% (base.get("commit", "?")[:12] or "?", len(base_lits), len(keys), len(preexisting),
len(named)))
len(named), judged))
if bad:
print("\ngo-parity gate CONVICTS (%d):" % len(bad))
for b in bad:
print(" " + b)
return 1
print("go-parity gate OK: every Go-side key carries base-commit text, byte for byte.")
print("go-parity gate OK: every Go-side key carries base-commit text, byte for byte, and every "
"judged call passes as many arguments as its message has verbs.")
return 0
+17 -1
View File
@@ -46,7 +46,8 @@ COVERS = {
"minagent-header": "the word MinAgent in prose / a code span, not the header line (test_minagent_header_gate.py)",
"i18n": "an undefined marker key, a pleading English value, a shrinking/growing gap (v0.247.0)",
"go-parity": "a Go-side key REWORDED, a key citing text no base literal has, and a converted "
"key left out of the map (v0.252.0, R-557)",
"key left out of the map (v0.252.0, R-557); a call that lost an argument, and a key glued "
"to more text with + (R-576)",
"gofmt": "R-454: a planted unformatted .go file in internal/ is convicted; the clean tree passes",
"offbox-rename": "R-425: NAS branding in a NEW backups*.html, and in a bundle value an offbox Go file "
"names; control: the same token in the network-storage feature's copy is accepted",
@@ -260,6 +261,21 @@ swapped("go-parity/invented", "i18n_go_parity.py", GO_KEYS,
swapped("go-parity/unlisted", "i18n_go_parity.py", GO_KEYS,
_one(_A_KEY + ",\n", ""))
# 4-5. R-576 -- the CALL lost text while every surviving fragment stayed real (2026-09-18, 7
# producers, this gate green). Built on a real producer: the update-already-running refusal.
UPDATE_GO = os.path.join(CTRL, "internal", "stacks", "update.go")
_R576 = 'util.MsgError("update.refusal.already", name), "lost the race'
# the argument dropped -- the key's text is still byte-equal, only the call is short.
swapped("go-parity/lost-argument", "i18n_go_parity.py", UPDATE_GO,
_one(_R576, 'util.MsgError("update.refusal.already"), "lost the race'))
# the key glued to a continuation with + -- the converter's exact shape.
swapped("go-parity/key-concat", "i18n_go_parity.py", UPDATE_GO,
_one(_R576, 'util.MsgError("update.refusal.already" + suffix, name), "lost the race'))
# CONTROL: one argument that itself holds parens and commas is still ONE argument.
swapped("go-parity/nested-arg-ok", "i18n_go_parity.py", UPDATE_GO,
_one(_R576, 'util.MsgError("update.refusal.already", fmt.Sprintf("%s,%s", f(a, b), c)), "lost the race'),
expect="accept")
# --- golden-notice: R-410's decoy, in the other direction. It is ADVISORY, so rc is never the ---
# --- question — what it COUNTED is. ---
ran += 1