| |
| """Final submission-readiness checks for the GRL manuscript package.""" |
|
|
| from __future__ import annotations |
|
|
| import json |
| import re |
| from pathlib import Path |
|
|
|
|
| ROOT = Path(__file__).resolve().parents[1] |
| MAIN = ROOT / "main.tex" |
| OUT = ROOT / "submission_final" |
| LOG = ROOT / "main.log" |
| PDF = ROOT / "main.pdf" |
|
|
| PLACEHOLDERS = [ |
| "TBD", |
| "to be added", |
| "Requires author confirmation", |
| "email to be added", |
| "funding details require author confirmation", |
| "Acknowledgment and funding details require author confirmation", |
| "should be added before submission", |
| "public repository DOI has not yet been assigned", |
| "data DOI: TBD", |
| "code DOI: TBD", |
| "INSERT REAL", |
| "INSERT FINAL", |
| ] |
|
|
| REQUIRED_ARCHIVE_TEXT = [ |
| "snr_bias", |
| "Hugging Face", |
| "SeismicX-Cont", |
| "10.57967/hf/9006", |
| "SeisDispFusion-NCF", |
| "10.57967/hf/9114", |
| "CREDIT-X1local", |
| ] |
|
|
| REQUIRED_FIGURES = [ |
| "figures/snr_matched_design.png", |
| "figures/fig2_training_combined.pdf", |
| "figures/fig2_training_combined.png", |
| "figures/fig3_continuous_association.pdf", |
| "figures/fig3_continuous_association.png", |
| ] |
|
|
| REQUIRED_SECTIONS = [ |
| "Plain Language Summary", |
| "Keywords", |
| "Introduction", |
| "Data and Methods", |
| "Results", |
| "Discussion and Conclusions", |
| "Open Research", |
| "Conflict of Interest declaration", |
| "AI Tool Disclosure", |
| ] |
|
|
| EXPECTED_CITATIONS = [ |
| "ross2018gpd", |
| "zhu2019phasenet", |
| "mousavi2020eqtransformer", |
| "li2024creditx1local", |
| "zhang2019real", |
| "yu2026seismicxcont", |
| "yu2026seisdispfusionncf", |
| "yu2026snrbias", |
| ] |
|
|
|
|
| def read(path: Path) -> str: |
| return path.read_text(encoding="utf-8", errors="replace") if path.exists() else "" |
|
|
|
|
| def strip_comments(text: str) -> str: |
| return re.sub(r"(?<!\\)%.*", "", text) |
|
|
|
|
| def strip_latex(text: str) -> str: |
| text = strip_comments(text) |
| text = text.replace(r"\%", " percent ") |
| text = re.sub(r"\\(?:cite|citeA|citep|citet)\{[^}]*\}", " ", text) |
| text = re.sub(r"\\path\{([^}]*)\}", r" \1 ", text) |
| text = re.sub(r"\\texttt\{([^}]*)\}", r" \1 ", text) |
| text = re.sub(r"\\[A-Za-z]+\*?(?:\[[^\]]*\])?(?:\{([^{}]*)\})?", r" \1 ", text) |
| text = re.sub(r"[{}$^_\\]", " ", text) |
| return text |
|
|
|
|
| def word_count(text: str) -> int: |
| clean = strip_latex(text) |
| return len(re.findall(r"[A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)?", clean)) |
|
|
|
|
| def extract_env(text: str, env: str) -> str: |
| match = re.search(rf"\\begin\{{{re.escape(env)}\}}(.*?)\\end\{{{re.escape(env)}\}}", text, re.S) |
| return match.group(1) if match else "" |
|
|
|
|
| def keypoints(text: str) -> list[str]: |
| body = extract_env(text, "keypoints") |
| return [strip_latex(item).strip() for item in re.findall(r"\\item\s+(.*)", body)] |
|
|
|
|
| def captions(text: str) -> list[str]: |
| items = re.findall(r"\\caption\{(.*?)\}", text, re.S) |
| table_file = ROOT / "continuous_assoc_table.tex" |
| if table_file.exists(): |
| items.extend(re.findall(r"\\caption\{(.*?)\}", table_file.read_text(encoding="utf-8"), re.S)) |
| return items |
|
|
|
|
| def counted_text_for_pu(text: str) -> str: |
| parts = [extract_env(text, "abstract")] |
| after_begin = text.split(r"\begin{document}", 1)[-1] |
| before_bib = after_begin.split(r"\bibliography", 1)[0] |
| before_bib = re.sub(r"\\title\{.*?\}", " ", before_bib, flags=re.S) |
| before_bib = re.sub(r"\\authors\{.*?\}", " ", before_bib, flags=re.S) |
| before_bib = re.sub(r"\\affiliation\{.*?\}\{.*?\}", " ", before_bib, flags=re.S) |
| before_bib = re.sub(r"\\correspondingauthor\{.*?\}\{.*?\}", " ", before_bib, flags=re.S) |
| before_bib = re.sub(r"\\begin\{keypoints\}.*?\\end\{keypoints\}", " ", before_bib, flags=re.S) |
| before_bib = re.sub(r"\\section\*\{Keywords\}.*?(?=\\section|\Z)", " ", before_bib, flags=re.S) |
| before_bib = re.sub(r"\\begin\{table\}.*?\\end\{table\}", " ", before_bib, flags=re.S) |
| before_bib = re.sub(r"\\begin\{tabular\}.*?\\end\{tabular\}", " ", before_bib, flags=re.S) |
| before_bib = re.sub(r"\\begin\{figure\}.*?\\end\{figure\}", " ", before_bib, flags=re.S) |
| parts.append(before_bib) |
| parts.extend(captions(text)) |
| return "\n".join(parts) |
|
|
|
|
| def unresolved_latex_issues(log_text: str) -> list[str]: |
| issues = [] |
| patterns = [ |
| "Undefined control sequence", |
| "LaTeX Error", |
| "Emergency stop", |
| "Fatal error", |
| "There were undefined references", |
| "There were undefined citations", |
| "Rerun to get cross-references right", |
| ] |
| for pattern in patterns: |
| if pattern in log_text: |
| issues.append(pattern) |
| if re.search(r"LaTeX Warning: (Citation|Reference) `[^']+' undefined", log_text): |
| issues.append("undefined citation/reference warning") |
| return issues |
|
|
|
|
| def main() -> int: |
| OUT.mkdir(parents=True, exist_ok=True) |
| text = read(MAIN) |
| bib_text = read(ROOT / "references.bib") |
| log_text = read(LOG) |
|
|
| failures: list[str] = [] |
| warnings: list[str] = [] |
| human_input: list[str] = [] |
|
|
| if not text: |
| failures.append("main.tex is missing or empty") |
|
|
| abstract_words = word_count(extract_env(text, "abstract")) |
| if abstract_words >= 150: |
| failures.append(f"Abstract is {abstract_words} words; GRL target is <150.") |
|
|
| kp = keypoints(text) |
| kp_lengths = [len(item) for item in kp] |
| if len(kp) > 3: |
| failures.append(f"Found {len(kp)} Key Points; maximum is 3.") |
| for idx, length in enumerate(kp_lengths, 1): |
| if length > 140: |
| failures.append(f"Key Point {idx} is {length} characters; maximum is 140.") |
|
|
| for token in PLACEHOLDERS: |
| if token.lower() in text.lower(): |
| failures.append(f"Placeholder remains in manuscript: {token}") |
|
|
| for section in REQUIRED_SECTIONS: |
| if section not in text: |
| failures.append(f"Required section missing: {section}") |
|
|
| if "yuziye@cea-igp.ac.cn" not in text: |
| failures.append("Corresponding email is missing.") |
| if "Institute of Geophysics, China Earthquake Administration" not in text: |
| failures.append("Affiliation is missing.") |
|
|
| for required in REQUIRED_ARCHIVE_TEXT: |
| if required not in text: |
| failures.append(f"Open Research missing required archive text: {required}") |
|
|
| if "No specific funding was received for this work" not in text: |
| failures.append("Funding statement is missing or not finalized.") |
| if "no separate license file was present" in text: |
| human_input.append("Code archive license is not assigned; add a LICENSE file/metadata if a reuse license is intended.") |
|
|
| for figure in REQUIRED_FIGURES: |
| if not (ROOT / figure).exists(): |
| failures.append(f"Required figure file missing: {figure}") |
|
|
| table_file = ROOT / "continuous_assoc_table.tex" |
| if not table_file.exists(): |
| failures.append("continuous_assoc_table.tex is missing.") |
| else: |
| table_text = read(table_file) |
| for value in ["576,875", "1,301", "0.556", "1,561", "0.667"]: |
| if value not in table_text: |
| failures.append(f"Continuous association table missing expected value: {value}") |
|
|
| for citation in EXPECTED_CITATIONS: |
| if citation not in text: |
| failures.append(f"Expected citation is not used in main.tex: {citation}") |
| if citation not in bib_text: |
| failures.append(f"Expected citation is missing from references.bib: {citation}") |
|
|
| if not PDF.exists(): |
| failures.append("main.pdf is missing.") |
| elif MAIN.exists() and PDF.stat().st_mtime < MAIN.stat().st_mtime: |
| failures.append("main.pdf is older than main.tex; recompile manuscript.") |
|
|
| latex_issues = unresolved_latex_issues(log_text) |
| if latex_issues: |
| failures.extend([f"LaTeX log issue: {issue}" for issue in latex_issues]) |
| if not log_text: |
| warnings.append("main.log not found; compile before running final check for LaTeX-log validation.") |
|
|
| counted_words = word_count(counted_text_for_pu(text)) |
| figure_count = len(re.findall(r"\\begin\{figure\}", text)) |
| table_count = len(re.findall(r"\\begin\{table\}", text)) + len(re.findall(r"\\input\{continuous_assoc_table\}", text)) |
| publication_units = counted_words / 500.0 + figure_count + table_count |
| if publication_units > 12: |
| failures.append(f"Publication units estimate is {publication_units:.2f}; maximum is 12.") |
| elif publication_units > 11.5: |
| warnings.append(f"Publication units estimate is {publication_units:.2f}; internal safety target is <=11.5.") |
|
|
| status = "FAIL" if failures else ("BLOCKED_FOR_HUMAN" if human_input else "PASS") |
| report = { |
| "overall_status": status, |
| "abstract_word_count": abstract_words, |
| "key_point_character_counts": kp_lengths, |
| "publication_units_estimate": round(publication_units, 3), |
| "counted_words_estimate": counted_words, |
| "figure_count": figure_count, |
| "table_count": table_count, |
| "compile_status": "checked_main_log" if log_text and not latex_issues else ("not_checked" if not log_text else "issues_found"), |
| "citation_status": "resolved" if not latex_issues else "check_log", |
| "placeholder_status": "clear" if not any(token.lower() in text.lower() for token in PLACEHOLDERS) else "found", |
| "open_research_status": "complete_required_dois_present", |
| "failures": failures, |
| "warnings": warnings, |
| "human_input_required": human_input, |
| } |
|
|
| (OUT / "final_submission_check.json").write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") |
|
|
| lines = [ |
| "# Final Submission Check", |
| "", |
| f"- Overall status: `{status}`", |
| f"- Abstract word count: {abstract_words}", |
| f"- Key Point character counts: {', '.join(map(str, kp_lengths))}", |
| f"- Publication unit estimate: {publication_units:.2f}", |
| f"- Counted words estimate: {counted_words}", |
| f"- Figures: {figure_count}", |
| f"- Tables: {table_count}", |
| f"- Compile status: {report['compile_status']}", |
| f"- Citation status: {report['citation_status']}", |
| f"- Placeholder status: {report['placeholder_status']}", |
| f"- Open Research status: {report['open_research_status']}", |
| "", |
| "## Failures", |
| "", |
| *([f"- {item}" for item in failures] or ["- None"]), |
| "", |
| "## Warnings", |
| "", |
| *([f"- {item}" for item in warnings] or ["- None"]), |
| "", |
| "## Human Input Required", |
| "", |
| *([f"- {item}" for item in human_input] or ["- None"]), |
| ] |
| (OUT / "final_submission_check.md").write_text("\n".join(lines) + "\n", encoding="utf-8") |
| print(json.dumps(report, indent=2)) |
| return 1 if status == "FAIL" else 0 |
|
|
|
|
| if __name__ == "__main__": |
| raise SystemExit(main()) |
|
|