Spaces:
Running
Running
| """Secret-safe static verification for the public release handoff.""" | |
| from __future__ import annotations | |
| import json | |
| from pathlib import Path | |
| import re | |
| import sys | |
| import tomllib | |
| ROOT = Path(__file__).resolve().parents[1] | |
| EXPECTED_RELEASE_VERSION = "1.1.3" | |
| EXPECTED_PROJECT_AUTHOR = "kuotunyu" | |
| EXPECTED_CODEOWNERS_RULES = ("* @kuotunyu",) | |
| REQUIRED_FILES = ( | |
| "README.md", | |
| "LICENSE", | |
| "CHANGELOG.md", | |
| ".github/CONTRIBUTING.md", | |
| ".github/SECURITY.md", | |
| "Dockerfile", | |
| ".dockerignore", | |
| ".env.example", | |
| ".github/workflows/ci.yml", | |
| ".github/CODEOWNERS", | |
| ".github/dependabot.yml", | |
| ".github/pull_request_template.md", | |
| "pyproject.toml", | |
| "uv.lock", | |
| "data/evaluation/decision_cases.json", | |
| "data/evaluation/provenance/manifest.json", | |
| "docs/CASE_STUDY.md", | |
| "docs/DECISION_EVALUATION.md", | |
| "docs/EVIDENCE_PROVENANCE.md", | |
| "docs/REMOTE_SETUP.md", | |
| "docs/assets/desktop.png", | |
| "docs/assets/evidence-provenance.png", | |
| "docs/assets/decision-evaluation.json", | |
| "docs/assets/provenance-evaluation.json", | |
| "docs/assets/xfund-extraction-benchmark.json", | |
| "docs/assets/colqwen-retrieval-benchmark.json", | |
| "scripts/audit_ui_quality.py", | |
| "scripts/build_provenance_corpus.py", | |
| "scripts/run_provenance_evaluation.py", | |
| "scripts/check_github_ci.py", | |
| "scripts/check_github_contributors.py", | |
| "scripts/check_live_space.py", | |
| "scripts/check_space_snapshot.py", | |
| "scripts/serve_ui_fixture.py", | |
| "scripts/verify_distribution.py", | |
| "scripts/verify_public_docs.py", | |
| "scripts/verify_ui_layout.py", | |
| ) | |
| UNNECESSARY_PUBLIC_FILES = ( | |
| "PRODUCT.md", | |
| "DEPLOYMENT.md", | |
| "docs/UI_AUDIT.md", | |
| "docs/RELEASE_NOTES_v1.0.0.md", | |
| "docs/assets/browser-report.json", | |
| "docs/assets/ui-quality-audit.json", | |
| "docs/assets/wide.png", | |
| "docs/assets/mobile.png", | |
| "docs/assets/result-red.png", | |
| ) | |
| REMOTE_SETUP_MARKERS = ( | |
| "GitHub 主倉 → Hugging Face 部署鏡像", | |
| "https://github.com/kuotunyu/doc-inspector", | |
| "https://huggingface.co/spaces/steven0226/doc-inspector", | |
| "git -c core.autocrlf=false archive", | |
| "git hash-object --no-filters", | |
| "uvx --from huggingface_hub hf upload", | |
| "--delete '*'", | |
| "不要再對 Space 使用 force push", | |
| "PUBLIC_MAX_REQUESTS_PER_HOUR=60", | |
| "scripts/check_live_space.py", | |
| "私人驗收清單", | |
| "回復方式", | |
| "Set-Location '<專案資料夾>'", | |
| "git config user.email", | |
| "Co-authored-by", | |
| "不要直接 merge、squash 或 rebase", | |
| "scripts/check_github_ci.py", | |
| "scripts/check_github_contributors.py", | |
| "scripts/check_space_snapshot.py", | |
| "line_ending_only_mismatches", | |
| "$spaceMetadata", | |
| "readme_metadata_valid", | |
| "readme_body_match", | |
| "https://github.com/kuotunyu/doc-inspector/graphs/contributors", | |
| "<VERSION>", | |
| "既有 tag 不可移動、刪除或重建", | |
| "$VerifiedReleaseCommit", | |
| "實際測試數與 coverage 以目前版本 README 及指定 SHA 的 CI log 為準", | |
| ) | |
| REMOTE_SETUP_FORBIDDEN_MARKERS = ( | |
| "git push hf", | |
| "git push --force-with-lease hf", | |
| "git archive --format=zip", | |
| ) | |
| README_MARKERS = ( | |
| "我常看到補助申請真正困難的地方", | |
| "## 架構", | |
| "```mermaid", | |
| "## 模型選型與台灣生態系對照", | |
| "## 快速開始", | |
| "## XFUND 評估", | |
| "## 成本", | |
| "## 隱私與安全邊界", | |
| "## Demo 資料與授權", | |
| "## CPU 容器與部署", | |
| "## 目前限制", | |
| "v1.1.3", | |
| "Hugging Face Docker Space", | |
| "266 passed,總 coverage 91%", | |
| "https://steven0226-doc-inspector.hf.space", | |
| "[![CI]", | |
| "## 可驗證成果", | |
| "## 決策層產品評估", | |
| "## 來源核驗", | |
| "docs/CASE_STUDY.md", | |
| "docs/DECISION_EVALUATION.md", | |
| "docs/EVIDENCE_PROVENANCE.md", | |
| "24 / 24", | |
| "false verified rate", | |
| "19 個非 README 關鍵檔 byte-exact", | |
| "README metadata/本文 exact match", | |
| ) | |
| HARDCODED_RELEASE_VERSION_PATTERNS = ( | |
| re.compile(r"\bgit tag(?:\s+-a)?\s+v\d+\.\d+\.\d+\b"), | |
| re.compile(r"\bgit push origin v\d+\.\d+\.\d+\b"), | |
| ) | |
| VERSION_SPECIFIC_TEST_EVIDENCE_PATTERNS = ( | |
| re.compile(r"\b\d+\s*項測試\b.*?\bcoverage\s+\d+(?:\.\d+)?%", re.IGNORECASE), | |
| re.compile(r"\b\d+\s+passed\b.*?\bcoverage\s+\d+(?:\.\d+)?%", re.IGNORECASE), | |
| ) | |
| CI_WORKFLOW_MARKERS = ( | |
| "contents: read", | |
| "ubuntu-latest", | |
| "windows-latest", | |
| "python-version: \"3.11\"", | |
| "uv lock --check", | |
| "uv sync --locked --all-groups", | |
| "--cov-fail-under=85", | |
| "python scripts/verify_deployment.py", | |
| "python -m compileall -q src scripts tests", | |
| "python scripts/run_product_evaluation.py --check", | |
| "python scripts/run_provenance_evaluation.py --check", | |
| "python scripts/verify_public_docs.py", | |
| "uv build --clear --no-build-isolation", | |
| "python scripts/verify_distribution.py", | |
| "python scripts/verify_release.py", | |
| ) | |
| CI_WORKFLOW_FORBIDDEN_MARKERS = ( | |
| "pull_request_target", | |
| "secrets.", | |
| ) | |
| DEPENDABOT_MARKERS = ( | |
| 'package-ecosystem: "uv"', | |
| 'package-ecosystem: "github-actions"', | |
| 'package-ecosystem: "docker"', | |
| ) | |
| CODEOWNERS_MARKERS = ( | |
| "* @kuotunyu", | |
| ) | |
| SECRET_PLACEHOLDERS = ( | |
| "GOOGLE_API_KEY", | |
| "GEMINI_API_KEY", | |
| "OPENAI_API_KEY", | |
| "HF_TOKEN", | |
| "DISCORD_WEBHOOK_URL", | |
| ) | |
| def _parse_env_example(text: str) -> dict[str, str]: | |
| values: dict[str, str] = {} | |
| for raw_line in text.splitlines(): | |
| line = raw_line.strip() | |
| if not line or line.startswith("#") or "=" not in line: | |
| continue | |
| key, value = line.split("=", 1) | |
| values[key.strip()] = value.strip() | |
| return values | |
| def _hardcoded_release_version_markers(text: str) -> list[str]: | |
| return sorted( | |
| { | |
| match.group(0) | |
| for pattern in HARDCODED_RELEASE_VERSION_PATTERNS | |
| for match in pattern.finditer(text) | |
| } | |
| ) | |
| def _version_specific_test_evidence_markers(text: str) -> list[str]: | |
| issues: set[str] = set() | |
| for raw_line in text.splitlines(): | |
| line = raw_line.strip().removeprefix("- ").strip() | |
| if any(pattern.search(line) for pattern in VERSION_SPECIFIC_TEST_EVIDENCE_PATTERNS): | |
| issues.add(line) | |
| return sorted(issues) | |
| def _public_readme_space_metadata_issues(text: str) -> list[str]: | |
| candidate = text.removeprefix("\ufeff") | |
| issues: list[str] = [] | |
| if candidate.startswith("---\n"): | |
| closing_index = candidate.find("\n---\n", 4) | |
| if closing_index != -1: | |
| frontmatter = candidate[4:closing_index] | |
| if "sdk: docker" in frontmatter or "app_port:" in frontmatter: | |
| issues.append("README.md:space-frontmatter") | |
| if re.search( | |
| r"20\s*[//]\s*20\s+Space\s+關鍵來源檔\s+byte-exact", | |
| candidate, | |
| ): | |
| issues.append("README.md:outdated-space-byte-exact-claim") | |
| return issues | |
| def build_release_report(root: Path = ROOT) -> dict[str, object]: | |
| missing_files = [path for path in REQUIRED_FILES if not (root / path).is_file()] | |
| unexpected_public_files = [ | |
| path for path in UNNECESSARY_PUBLIC_FILES if (root / path).is_file() | |
| ] | |
| pyproject = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8")) | |
| lockfile = tomllib.loads((root / "uv.lock").read_text(encoding="utf-8")) | |
| project_version = pyproject.get("project", {}).get("version") | |
| project_authors = pyproject.get("project", {}).get("authors", []) | |
| project_author = ( | |
| project_authors[0].get("name") | |
| if len(project_authors) == 1 and isinstance(project_authors[0], dict) | |
| else None | |
| ) | |
| locked_project_versions = [ | |
| package.get("version") | |
| for package in lockfile.get("package", []) | |
| if package.get("name") == "doc-inspector" | |
| ] | |
| release_version_issues: list[str] = [] | |
| if project_version != EXPECTED_RELEASE_VERSION: | |
| release_version_issues.append("pyproject.toml") | |
| if locked_project_versions != [EXPECTED_RELEASE_VERSION]: | |
| release_version_issues.append("uv.lock") | |
| project_author_issues = [] | |
| if project_author != EXPECTED_PROJECT_AUTHOR: | |
| project_author_issues.append("pyproject.toml") | |
| readme = (root / "README.md").read_text(encoding="utf-8") | |
| missing_readme_markers = [ | |
| marker for marker in README_MARKERS if marker not in readme | |
| ] | |
| public_readme_issues = _public_readme_space_metadata_issues(readme) | |
| ci_workflow = (root / ".github" / "workflows" / "ci.yml").read_text( | |
| encoding="utf-8" | |
| ) | |
| missing_ci_workflow_markers = [ | |
| marker for marker in CI_WORKFLOW_MARKERS if marker not in ci_workflow | |
| ] | |
| forbidden_ci_workflow_markers = [ | |
| marker for marker in CI_WORKFLOW_FORBIDDEN_MARKERS if marker in ci_workflow | |
| ] | |
| dependabot = (root / ".github" / "dependabot.yml").read_text(encoding="utf-8") | |
| missing_dependabot_markers = [ | |
| marker for marker in DEPENDABOT_MARKERS if marker not in dependabot | |
| ] | |
| codeowners = (root / ".github" / "CODEOWNERS").read_text(encoding="utf-8") | |
| missing_codeowners_markers = [ | |
| marker for marker in CODEOWNERS_MARKERS if marker not in codeowners | |
| ] | |
| codeowners_rules = tuple( | |
| line.strip() | |
| for line in codeowners.splitlines() | |
| if line.strip() and not line.lstrip().startswith("#") | |
| ) | |
| codeowners_issues = [] | |
| if codeowners_rules != EXPECTED_CODEOWNERS_RULES: | |
| codeowners_issues.append(".github/CODEOWNERS") | |
| decision_evaluation = json.loads( | |
| (root / "docs" / "assets" / "decision-evaluation.json").read_text( | |
| encoding="utf-8" | |
| ) | |
| ) | |
| decision_cases = json.loads( | |
| (root / "data" / "evaluation" / "decision_cases.json").read_text( | |
| encoding="utf-8" | |
| ) | |
| ) | |
| decision_evaluation_issues: list[str] = [] | |
| if decision_evaluation.get("passed") is not True: | |
| decision_evaluation_issues.append("passed") | |
| if decision_evaluation.get("uses_network") is not False: | |
| decision_evaluation_issues.append("uses_network") | |
| if decision_evaluation.get("uses_api_keys") is not False: | |
| decision_evaluation_issues.append("uses_api_keys") | |
| if "人工" not in decision_evaluation.get("oracle_method", ""): | |
| decision_evaluation_issues.append("oracle_method") | |
| if decision_evaluation.get("oracle_method") != decision_cases.get("oracle_method"): | |
| decision_evaluation_issues.append("oracle_method.drift") | |
| if decision_evaluation.get("benchmark_version") != decision_cases.get("version"): | |
| decision_evaluation_issues.append("benchmark_version") | |
| metrics = decision_evaluation.get("metrics", {}) | |
| expected_case_count = len(decision_cases.get("cases", [])) | |
| if metrics.get("case_count") != expected_case_count or expected_case_count < 20: | |
| decision_evaluation_issues.append("metrics.case_count") | |
| for metric_name in ( | |
| "exact_case_match_rate", | |
| "overall_status_accuracy", | |
| "issue_precision", | |
| "issue_recall", | |
| "red_issue_recall", | |
| "yellow_issue_recall", | |
| ): | |
| if metrics.get(metric_name) != 1.0: | |
| decision_evaluation_issues.append(f"metrics.{metric_name}") | |
| if any(case.get("passed") is not True for case in decision_evaluation.get("cases", [])): | |
| decision_evaluation_issues.append("cases.passed") | |
| provenance_evaluation = json.loads( | |
| (root / "docs" / "assets" / "provenance-evaluation.json").read_text( | |
| encoding="utf-8" | |
| ) | |
| ) | |
| provenance_corpus = json.loads( | |
| (root / "data" / "evaluation" / "provenance" / "manifest.json").read_text( | |
| encoding="utf-8" | |
| ) | |
| ) | |
| provenance_issues: list[str] = [] | |
| if provenance_evaluation.get("passed") is not True: | |
| provenance_issues.append("provenance.passed") | |
| if provenance_evaluation.get("gate_failures"): | |
| provenance_issues.append("provenance.gate_failures") | |
| if provenance_evaluation.get("corpus_checksums_verified") is not True: | |
| provenance_issues.append("provenance.corpus_checksums_verified") | |
| for flag in ("uses_network", "uses_api_keys", "uses_gpu"): | |
| if provenance_evaluation.get(flag) is not False: | |
| provenance_issues.append(f"provenance.{flag}") | |
| if provenance_evaluation.get("corpus_version") != provenance_corpus.get("version"): | |
| provenance_issues.append("provenance.corpus_version") | |
| if provenance_corpus.get("contains_real_personal_data") is not False: | |
| provenance_issues.append("provenance.contains_real_personal_data") | |
| if provenance_corpus.get("localizable_field_count", 0) < 40: | |
| provenance_issues.append("provenance.localizable_field_count") | |
| provenance_metrics = provenance_evaluation.get("metrics", {}) | |
| if provenance_metrics.get("false_verified_rate") != 0.0: | |
| provenance_issues.append("provenance.false_verified_rate") | |
| if provenance_metrics.get("page_localization_accuracy", 0.0) < 0.95: | |
| provenance_issues.append("provenance.page_localization_accuracy") | |
| if provenance_metrics.get("verified_bbox_hit_rate", 0.0) < 0.90: | |
| provenance_issues.append("provenance.verified_bbox_hit_rate") | |
| if any( | |
| outcome.get("false_verified") is True | |
| for outcome in provenance_evaluation.get("outcomes", []) | |
| ): | |
| provenance_issues.append("provenance.outcomes.false_verified") | |
| if not (root / "data" / "evaluation" / "provenance").glob("*.pdf"): | |
| provenance_issues.append("provenance.corpus_documents") | |
| for entry in provenance_corpus.get("documents", []): | |
| document_path = root / "data" / "evaluation" / "provenance" / str(entry.get("file")) | |
| if not document_path.is_file(): | |
| provenance_issues.append(f"provenance.missing:{entry.get('file')}") | |
| remote_setup = (root / "docs" / "REMOTE_SETUP.md").read_text(encoding="utf-8") | |
| missing_remote_setup_markers = [ | |
| marker for marker in REMOTE_SETUP_MARKERS if marker not in remote_setup | |
| ] | |
| forbidden_remote_setup_markers = [ | |
| marker for marker in REMOTE_SETUP_FORBIDDEN_MARKERS if marker in remote_setup | |
| ] | |
| forbidden_remote_setup_markers.extend( | |
| _hardcoded_release_version_markers(remote_setup) | |
| ) | |
| forbidden_remote_setup_markers.extend( | |
| _version_specific_test_evidence_markers(remote_setup) | |
| ) | |
| dockerfile = (root / "Dockerfile").read_text(encoding="utf-8") | |
| missing_public_safety_markers = [ | |
| marker | |
| for marker in ( | |
| "PUBLIC_MAX_REQUESTS_PER_HOUR=60", | |
| "USER appuser", | |
| ) | |
| if marker not in dockerfile | |
| ] | |
| env_values = _parse_env_example( | |
| (root / ".env.example").read_text(encoding="utf-8") | |
| ) | |
| missing_secret_placeholders = [ | |
| key for key in SECRET_PLACEHOLDERS if key not in env_values | |
| ] | |
| non_empty_secret_placeholders = [ | |
| key for key in SECRET_PLACEHOLDERS if env_values.get(key) | |
| ] | |
| public_handoff_text = "\n".join( | |
| ( | |
| readme, | |
| remote_setup, | |
| ) | |
| ) | |
| private_path_markers = [ | |
| marker for marker in ("C:\\Users\\", "/Users/", "/home/") if marker in public_handoff_text | |
| ] | |
| gitignore = (root / ".gitignore").read_text(encoding="utf-8").splitlines() | |
| dockerignore = (root / ".dockerignore").read_text(encoding="utf-8").splitlines() | |
| missing_gitignore_rules = [ | |
| rule | |
| for rule in ( | |
| ".env", | |
| ".env.*", | |
| ".agents/", | |
| "AGENTS.md", | |
| "CLAUDE.md", | |
| "PLAN.md", | |
| "PROGRESS.md", | |
| "data/raw/", | |
| "outputs/", | |
| "logs/", | |
| ) | |
| if rule not in gitignore | |
| ] | |
| missing_dockerignore_rules = [ | |
| rule | |
| for rule in ( | |
| ".env", | |
| ".env.*", | |
| ".agents/", | |
| "AGENTS.md", | |
| "CLAUDE.md", | |
| "PLAN.md", | |
| "PROGRESS.md", | |
| "data/", | |
| "outputs/", | |
| "logs/", | |
| ) | |
| if rule not in dockerignore | |
| ] | |
| issues = ( | |
| missing_files | |
| + unexpected_public_files | |
| + release_version_issues | |
| + project_author_issues | |
| + missing_readme_markers | |
| + public_readme_issues | |
| + missing_ci_workflow_markers | |
| + forbidden_ci_workflow_markers | |
| + missing_dependabot_markers | |
| + missing_codeowners_markers | |
| + codeowners_issues | |
| + decision_evaluation_issues | |
| + provenance_issues | |
| + missing_remote_setup_markers | |
| + forbidden_remote_setup_markers | |
| + missing_public_safety_markers | |
| + private_path_markers | |
| + missing_secret_placeholders | |
| + non_empty_secret_placeholders | |
| + missing_gitignore_rules | |
| + missing_dockerignore_rules | |
| ) | |
| return { | |
| "ready_for_manual_handoff": not issues, | |
| "required_file_count": len(REQUIRED_FILES), | |
| "missing_files": missing_files, | |
| "unexpected_public_files": unexpected_public_files, | |
| "expected_release_version": EXPECTED_RELEASE_VERSION, | |
| "project_version": project_version, | |
| "locked_project_versions": locked_project_versions, | |
| "release_version_issues": release_version_issues, | |
| "project_author": project_author, | |
| "project_author_issues": project_author_issues, | |
| "missing_readme_markers": missing_readme_markers, | |
| "public_readme_issues": public_readme_issues, | |
| "missing_ci_workflow_markers": missing_ci_workflow_markers, | |
| "forbidden_ci_workflow_markers": forbidden_ci_workflow_markers, | |
| "missing_dependabot_markers": missing_dependabot_markers, | |
| "missing_codeowners_markers": missing_codeowners_markers, | |
| "codeowners_rules": codeowners_rules, | |
| "codeowners_issues": codeowners_issues, | |
| "decision_evaluation_issues": decision_evaluation_issues, | |
| "provenance_evaluation_issues": provenance_issues, | |
| "missing_remote_setup_markers": missing_remote_setup_markers, | |
| "forbidden_remote_setup_markers": forbidden_remote_setup_markers, | |
| "missing_public_safety_markers": missing_public_safety_markers, | |
| "private_path_markers": private_path_markers, | |
| "missing_secret_placeholders": missing_secret_placeholders, | |
| "non_empty_secret_placeholders": non_empty_secret_placeholders, | |
| "missing_gitignore_rules": missing_gitignore_rules, | |
| "missing_dockerignore_rules": missing_dockerignore_rules, | |
| "reads_env_truth": False, | |
| "performs_network_calls": False, | |
| } | |
| def main() -> int: | |
| if hasattr(sys.stdout, "reconfigure"): | |
| sys.stdout.reconfigure(encoding="utf-8") | |
| report = build_release_report() | |
| print(json.dumps(report, ensure_ascii=False)) | |
| return 0 if report["ready_for_manual_handoff"] else 1 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |