@@ -50,6 +50,10 @@ def strict_policy(repo: Path, revision: str) -> bool:
5050 return cast (int , read_policy (repo , revision )["schema_version" ]) == 2
5151
5252
53+ def go_ratchet (repo : Path , revision : str ) -> bool :
54+ return cast (bool , read_policy (repo , revision ).get ("ratchet" ) == "go" )
55+
56+
5357MANIFEST = b"scripts/castiron/generated-files.json"
5458# Accept the relocated tooling only from the verified checkpoint. Deploy this
5559# reader before changing the producer so trusted-main checks can validate moves.
@@ -266,6 +270,8 @@ def check_isolation(repo: Path, base: str, head: str) -> int | None:
266270 return None
267271 if strict_policy (repo , base ) and not strict_policy (repo , head ):
268272 raise ValueError ("strict generated-file ownership cannot be downgraded" )
273+ if go_ratchet (repo , base ) and not go_ratchet (repo , head ):
274+ raise ValueError ("Go customization ratchet cannot be disabled" )
269275 if paths != {POLICY .encode ()}:
270276 raise ValueError (ISOLATION_MESSAGE )
271277 return proposed
@@ -290,6 +296,40 @@ def outcome(state: str, description: str) -> dict[str, str]:
290296 return {"state" : state , "description" : description }
291297
292298
299+ def go_customization (repo : Path , revision : str , measured : dict [str , Any ]) -> dict [str , Any ]:
300+ """Count patches plus physical handwritten Go, using only verified Git objects."""
301+ added , removed = count_custom_lines (measured )
302+ # Use the reporter's ownership view. Private snapshots also retain SDK-owned
303+ # CI files excluded from that view; those Go files are still handwritten.
304+ generated = report .tree_entries (repo , measured ["after" ]["commit" ])
305+ handwritten = {
306+ path : entry
307+ for path , entry in report .tree_entries (repo , revision , include_workflows = True ).items ()
308+ if path .endswith (b".go" ) and path not in generated
309+ }
310+ for path , entry in handwritten .items ():
311+ if entry .mode not in {b"100644" , b"100755" }:
312+ raise ValueError (f"nonregular handwritten Go: { path !r} " )
313+ blobs = report .read_blobs (repo , handwritten )
314+ files : list [dict [str , Any ]] = []
315+ for path , entry in sorted (handwritten .items ()):
316+ content = blobs [entry .oid ]
317+ content .decode ("utf-8" )
318+ if b"\0 " in content :
319+ raise ValueError (f"non-text handwritten Go: { path !r} " )
320+ lines = content .count (b"\n " ) + int (bool (content ) and not content .endswith (b"\n " ))
321+ files .append ({"path" : path .decode ("utf-8" ), "lines" : lines })
322+ handwritten_lines = sum (file ["lines" ] for file in files )
323+ return {
324+ "revision" : revision ,
325+ "generated" : measured ["after" ],
326+ "patch_lines" : added + removed ,
327+ "handwritten_lines" : handwritten_lines ,
328+ "handwritten_files" : files ,
329+ "total" : added + removed + handwritten_lines ,
330+ }
331+
332+
293333def evaluate (
294334 repo : Path ,
295335 base : str ,
@@ -362,11 +402,38 @@ def evaluate(
362402 result ["checked_limit" ] = checked_limit
363403 state = "success" if total <= checked_limit else "failure"
364404 description = f"+{ added } / -{ removed } = { total } custom lines; limit { checked_limit } ."
405+ if any (go_ratchet (repo , revision ) for revision in (policy , base , head )):
406+ # The report's historical merge base is for presentation only. Measure
407+ # the captured live target against its own hash-verified snapshot.
408+ baseline , _ = report .build_report (
409+ repo ,
410+ base ,
411+ base ,
412+ public = public ,
413+ fetch = fetch ,
414+ require_head_hash = True ,
415+ target_branch = target_branch ,
416+ )
417+ before = go_customization (repo , base , baseline )
418+ after = go_customization (repo , head , measured )
419+ delta = after ["total" ] - before ["total" ]
420+ result ["ratchet" ] = {"kind" : "go" , "before" : before , "after" : after , "delta" : delta }
421+ description += (
422+ f" Go customization { before ['total' ]} -> { after ['total' ]} (delta { delta :+d} )."
423+ )
424+ if delta > 0 :
425+ state = "failure"
426+ if go_ratchet (repo , base ) and not go_ratchet (repo , head ):
427+ state = "failure"
428+ description += " Go customization ratchet cannot be disabled."
365429 # Main's strict policy applies even if the candidate tries to remove it.
366430 # A proposed activation must itself satisfy the stricter contract.
367431 if strict_policy (repo , policy ) or strict_policy (repo , base ) or strict_policy (repo , head ):
368432 violations = verify_generated (
369- repo , measured ["after" ]["commit" ], head , previous = measured ["before" ]["commit" ]
433+ repo ,
434+ measured ["after" ]["commit" ],
435+ head ,
436+ previous = (measured ["before" ] or measured ["adoption_baseline" ])["commit" ],
370437 )
371438 result ["strict_violations" ] = violations
372439 if violations :
@@ -512,24 +579,23 @@ def github_evaluate(
512579 f"{ branch } must require a merge queue before activating the budget gate"
513580 )
514581 page += 1
515- if branch == "next" :
516- # This receipt is created locally by the trusted-main selector before the
517- # pinned checkout. It must never come from candidate workflow artifacts.
518- if (
519- selection is None
520- or type (selection .get ("schema_version" )) is not int
521- or any (
522- selection .get (key ) != value
523- for key , value in {
524- "schema_version" : 1 ,
525- "target_branch" : branch ,
526- "base_sha" : base ,
527- "head_sha" : head ,
528- "policy_sha" : main ,
529- }.items ()
530- )
531- ):
532- raise ValueError ("trusted next policy selection does not match evaluation" )
582+ # This receipt is created locally by the trusted-main selector before the
583+ # pinned checkout. It must never come from candidate workflow artifacts.
584+ if (
585+ selection is None
586+ or type (selection .get ("schema_version" )) is not int
587+ or any (
588+ selection .get (key ) != value
589+ for key , value in {
590+ "schema_version" : 1 ,
591+ "target_branch" : branch ,
592+ "base_sha" : base ,
593+ "head_sha" : head ,
594+ "policy_sha" : main ,
595+ }.items ()
596+ )
597+ ):
598+ raise ValueError ("trusted policy selection does not match evaluation" )
533599 measurement = None
534600 if trusted_report_dir is not None :
535601 # Only the preceding main-branch step may supply this directory. Never
@@ -556,19 +622,6 @@ def github_evaluate(
556622 report .git (
557623 repo , "fetch" , "--quiet" , "--no-tags" , "origin" , * dict .fromkeys ((main , base , head ))
558624 )
559- if branch == "next" :
560- assert selection is not None
561- base_pin = read_policy (repo , base ).get ("next_policy" )
562- if base_pin is None :
563- if selection .get ("bootstrap" ) is not True or changed_paths (repo , base , head ) != {
564- POLICY .encode ()
565- }:
566- raise ValueError ("unreviewed next policy initialization" )
567- elif selection .get ("bootstrap" ) is not False or base_pin != {
568- "repository" : repository ,
569- "main_sha" : main ,
570- }:
571- raise ValueError ("trusted next base pin changed; rerun policy selection" )
572625 pull_heads : list [str ] | None = None
573626 if run ["event" ] == "merge_group" :
574627 if report .git (repo , "merge-base" , base , head ).decode ().strip () != base :
@@ -585,6 +638,22 @@ def github_evaluate(
585638 pull_heads .append (candidate )
586639 if not pull_heads :
587640 raise ValueError ("cannot verify merge-group PR membership; retry the current group" )
641+ if branch == "next" :
642+ assert selection is not None
643+ base_pin = read_policy (repo , base ).get ("next_policy" )
644+ if base_pin is None :
645+ # evaluate() checks each verified queue constituent for policy isolation.
646+ if (
647+ selection .get ("bootstrap" ) is not True
648+ or "next_policy" not in read_policy (repo , head )
649+ or (pull_heads is None and changed_paths (repo , base , head ) != {POLICY .encode ()})
650+ ):
651+ raise ValueError ("unreviewed next policy initialization" )
652+ elif selection .get ("bootstrap" ) is not False or base_pin != {
653+ "repository" : repository ,
654+ "main_sha" : main ,
655+ }:
656+ raise ValueError ("trusted next base pin changed; rerun policy selection" )
588657 result , patch = evaluate (
589658 repo ,
590659 base ,
@@ -602,7 +671,7 @@ def github_evaluate(
602671
603672def main () -> int :
604673 if sys .argv [1 :2 ] == ["select-policy" ]:
605- return policy .main (sys .argv [1 :])
674+ return cast ( int , policy .main (sys .argv [1 :]) )
606675 parser = argparse .ArgumentParser (description = __doc__ )
607676 commands = parser .add_subparsers (dest = "command" , required = True )
608677 generated = commands .add_parser ("verify-generated" )
0 commit comments