8686 r"Public\s+Score[-\s]?File\s+Boundary:\s+MoFit" ,
8787 r"MoFit\s+is\s+a\s+support-only\s+score-file\s+diagnostic" ,
8888]
89+ PHASE_G_PDF_FORBIDDEN_PATTERNS = [
90+ r"D:\\" ,
91+ r"C:\\" ,
92+ r"Users\\" ,
93+ r"Documents\\" ,
94+ r"\bsecret\b" ,
95+ r"\bapi[_ -]?key\b" ,
96+ r"\btoken\b" ,
97+ r"\bHotCRP\b" ,
98+ r"\bCodex\b" ,
99+ r"\bChatGPT\b" ,
100+ r"\bClaude\b" ,
101+ r"\bsubmission\s+deadline\b" ,
102+ r"USENIX\s+Security\s+2027" ,
103+ r"NDSS\s+2027\s+fall" ,
104+ ]
105+ PHASE_G_PDF_REQUIRED_PATTERNS = [
106+ r"Run\s+Identity\s+in\s+Diffusion\s+Membership\s+Inference" ,
107+ r"training\s+run\s+identity\s+(?:is\s+)?an\s+audit\s+variable" ,
108+ r"AUC\s+0\.648" ,
109+ r"AUC\s+0\.81" ,
110+ r"candidate-positive" ,
111+ r"run-sensitive" ,
112+ r"single\s+evaluator" ,
113+ ]
89114LOG_FORBIDDEN_PATTERNS = [
90115 r"Undefined" ,
91116 r"Citation.*undefined" ,
@@ -1935,6 +1960,73 @@ def validate_bibliography(paper: Path, errors: list[str]) -> None:
19351960 require (not BIB_PLACEHOLDER_RE .search (entry ), f"{ key } contains placeholder BibTeX text" , errors )
19361961
19371962
1963+ def validate_phase_g_manifest_inputs (paper : Path , manifest : dict , errors : list [str ]) -> None :
1964+ excluded = {
1965+ normalize_manifest_relpath (str (path ), errors )
1966+ for path in manifest .get ("anonymous_supplement_excluded" , [])
1967+ if isinstance (manifest .get ("anonymous_supplement_excluded" , []), list )
1968+ }
1969+ requested : list [str ] = []
1970+ for category in ("generated" , "curated" , "paper_sources" ):
1971+ paths = manifest .get (category )
1972+ require (isinstance (paths , list ) and paths , f"manifest.{ category } must be a non-empty list" , errors )
1973+ if isinstance (paths , list ):
1974+ requested .extend (str (path ) for path in paths )
1975+ requested .extend (["asset_manifest.json" , "paper.pdf" ])
1976+
1977+ for raw_rel in requested :
1978+ rel = normalize_manifest_relpath (raw_rel , errors )
1979+ if rel in excluded :
1980+ continue
1981+ path = paper / rel
1982+ require (path .exists (), f"phase-g supplement input missing: { rel } " , errors )
1983+ if not path .exists () or path .suffix .lower () not in {".bib" , ".csv" , ".json" , ".md" , ".tex" , ".txt" }:
1984+ continue
1985+ text = path .read_text (encoding = "utf-8" , errors = "replace" )
1986+ for pattern in (r"(?<![A-Za-z0-9_])[A-Za-z]:(?:\\\\|\\)[A-Za-z0-9_. -]" , r"\\Users\\[A-Za-z0-9_. -]" , r"OPENAI_API_KEY" , r"sk-[A-Za-z0-9_-]{12,}" ):
1987+ require (not re .search (pattern , text , flags = re .IGNORECASE ), f"{ rel } contains private-surface pattern: { pattern } " , errors )
1988+
1989+
1990+ def validate_phase_g_pdf (paper : Path , errors : list [str ]) -> None :
1991+ pdf_path = paper / "paper.pdf"
1992+ require (pdf_path .exists (), "paper.pdf is missing" , errors )
1993+ if not pdf_path .exists ():
1994+ return
1995+
1996+ pdfinfo = run_text (["pdfinfo" , str (pdf_path )], paper )
1997+ pages_match = re .search (r"^Pages:\s+(\d+)$" , pdfinfo , flags = re .MULTILINE )
1998+ require (bool (pages_match ), "pdfinfo output missing Pages" , errors )
1999+ if pages_match :
2000+ pages = int (pages_match .group (1 ))
2001+ require (1 <= pages <= 20 , f"paper.pdf page count outside Phase G guardrail: { pages } " , errors )
2002+
2003+ pdf_text = run_text (["pdftotext" , str (pdf_path ), "-" ], paper )
2004+ for pattern in PHASE_G_PDF_FORBIDDEN_PATTERNS :
2005+ require (not re .search (pattern , pdf_text , flags = re .IGNORECASE ), f"paper.pdf private/submission-planning hit: { pattern } " , errors )
2006+ for pattern in PHASE_G_PDF_REQUIRED_PATTERNS :
2007+ require (bool (re .search (pattern , pdf_text , flags = re .IGNORECASE )), f"paper.pdf missing Phase G phrase: { pattern } " , errors )
2008+
2009+
2010+ def validate_phase_g_latex_log (paper : Path , errors : list [str ]) -> None :
2011+ candidates = [paper / "main.log" , paper / "build" / "main.log" ]
2012+ log_path = next ((path for path in candidates if path .exists ()), candidates [0 ])
2013+ require (log_path .exists (), "main.log is missing" , errors )
2014+ if not log_path .exists ():
2015+ return
2016+ text = log_path .read_text (encoding = "utf-8" , errors = "replace" )
2017+ for pattern in LOG_FORBIDDEN_PATTERNS :
2018+ require (not re .search (pattern , text , flags = re .IGNORECASE ), f"LaTeX log forbidden hit: { pattern } " , errors )
2019+
2020+
2021+ def validate_phase_g_release_packet (paper : Path , errors : list [str ]) -> None :
2022+ manifest = validate_manifest (paper , errors )
2023+ if manifest :
2024+ validate_phase_g_manifest_inputs (paper , manifest , errors )
2025+ validate_phase_g_pdf (paper , errors )
2026+ validate_phase_g_latex_log (paper , errors )
2027+ validate_bibliography (paper , errors )
2028+
2029+
19382030def validate_reference_integrity_audit (paper : Path , errors : list [str ]) -> None :
19392031 refs_path = paper / "refs.bib"
19402032 audit_path = paper / REFERENCE_INTEGRITY_AUDIT
@@ -2216,6 +2308,12 @@ def validate_c14_external_review_status(
22162308
22172309def main () -> None :
22182310 parser = argparse .ArgumentParser (description = __doc__ )
2311+ parser .add_argument (
2312+ "--profile" ,
2313+ choices = ["legacy-c14" , "phase-g" ],
2314+ default = "legacy-c14" ,
2315+ help = "Validation profile. legacy-c14 preserves the original C14 release gate; phase-g checks the current H1/run-identity manuscript packet." ,
2316+ )
22192317 parser .add_argument (
22202318 "--c14-review-state" ,
22212319 choices = ["pre-label" , "post-label" ],
@@ -2241,6 +2339,15 @@ def main() -> None:
22412339 )
22422340 errors : list [str ] = []
22432341
2342+ if args .profile == "phase-g" :
2343+ validate_phase_g_release_packet (paper , errors )
2344+ if errors :
2345+ for error in errors :
2346+ print (f"ERROR: { error } " )
2347+ raise SystemExit (1 )
2348+ print ("Phase G release packet check passed." )
2349+ return
2350+
22442351 manifest = validate_manifest (paper , errors )
22452352 validate_claim_trace (paper , repo_root , errors )
22462353 validate_review_snapshot_manifest (paper , repo_root , manifest , errors )
0 commit comments