scripts/report_scaffold.py
4.1 KB| 1 | #!/usr/bin/env python3 |
| 2 | """Generate a rigorous, redacted EDA Markdown report scaffold.""" |
| 3 | |
| 4 | from __future__ import annotations |
| 5 | |
| 6 | import argparse |
| 7 | from pathlib import Path |
| 8 | |
| 9 | from _common import ( |
| 10 | DEFAULT_MAX_FILE_BYTES, |
| 11 | CliError, |
| 12 | bounded_file_limit, |
| 13 | checked_input_file, |
| 14 | emit_markdown, |
| 15 | markdown_scalar, |
| 16 | run_cli, |
| 17 | ) |
| 18 | from capability_manifest import inspect_manifest |
| 19 | |
| 20 | |
| 21 | TEMPLATE = Path(__file__).resolve().parents[1] / "assets" / "report_template.md" |
| 22 | |
| 23 | |
| 24 | def build_parser() -> argparse.ArgumentParser: |
| 25 | parser = argparse.ArgumentParser( |
| 26 | description=( |
| 27 | "Create a local Markdown EDA scaffold that records design, missingness, " |
| 28 | "censoring, leakage, sensitivity, multiplicity, and reproducibility." |
| 29 | ) |
| 30 | ) |
| 31 | parser.add_argument( |
| 32 | "--input", |
| 33 | help="Optional local data file used only for a redacted capability manifest", |
| 34 | ) |
| 35 | parser.add_argument( |
| 36 | "--root", |
| 37 | default=".", |
| 38 | help="Existing local directory that bounds all input/output paths", |
| 39 | ) |
| 40 | parser.add_argument( |
| 41 | "--max-bytes", |
| 42 | type=int, |
| 43 | default=DEFAULT_MAX_FILE_BYTES, |
| 44 | help=f"Maximum optional input bytes (hard ceiling: {512 * 1024 * 1024})", |
| 45 | ) |
| 46 | parser.add_argument( |
| 47 | "--analysis-date", |
| 48 | default="not supplied", |
| 49 | help="Explicit date label (for example 2026-07-23); not generated implicitly", |
| 50 | ) |
| 51 | parser.add_argument( |
| 52 | "--reveal-identifiers", |
| 53 | action="store_true", |
| 54 | help="Include only a sanitized input basename; never include a full path", |
| 55 | ) |
| 56 | parser.add_argument( |
| 57 | "--output", |
| 58 | required=True, |
| 59 | help="Required local .md output path inside --root", |
| 60 | ) |
| 61 | parser.add_argument( |
| 62 | "--force", |
| 63 | action="store_true", |
| 64 | help="Allow replacement of an existing regular output file", |
| 65 | ) |
| 66 | return parser |
| 67 | |
| 68 | |
| 69 | def render_scaffold( |
| 70 | *, |
| 71 | analysis_date: str, |
| 72 | manifest: dict | None, |
| 73 | ) -> str: |
| 74 | """Fill only trusted scalar placeholders in the bundled template.""" |
| 75 | |
| 76 | try: |
| 77 | template = TEMPLATE.read_text(encoding="utf-8") |
| 78 | except OSError as exc: |
| 79 | raise CliError("the bundled report template is unavailable") from exc |
| 80 | if manifest is None: |
| 81 | replacements = { |
| 82 | "{ANALYSIS_DATE}": markdown_scalar(analysis_date), |
| 83 | "{FILE_ID}": "not supplied", |
| 84 | "{BASENAME}": "not supplied", |
| 85 | "{FORMAT}": "not supplied", |
| 86 | "{CAPABILITY_TIER}": "not supplied", |
| 87 | "{FILE_SIZE_BYTES}": "not supplied", |
| 88 | "{SIGNATURE_CHECKED}": "not supplied", |
| 89 | } |
| 90 | else: |
| 91 | file_info = manifest["file"] |
| 92 | capability = manifest["capability"] |
| 93 | replacements = { |
| 94 | "{ANALYSIS_DATE}": markdown_scalar(analysis_date), |
| 95 | "{FILE_ID}": markdown_scalar(str(file_info["file_id"])), |
| 96 | "{BASENAME}": markdown_scalar(str(file_info["basename"])), |
| 97 | "{FORMAT}": markdown_scalar(str(capability["format"])), |
| 98 | "{CAPABILITY_TIER}": markdown_scalar(str(capability["tier"])), |
| 99 | "{FILE_SIZE_BYTES}": str(file_info["size_bytes"]), |
| 100 | "{SIGNATURE_CHECKED}": str( |
| 101 | manifest["inspection"]["format_signature_checked"] |
| 102 | ).lower(), |
| 103 | } |
| 104 | rendered = template |
| 105 | for placeholder, value in replacements.items(): |
| 106 | rendered = rendered.replace(placeholder, value) |
| 107 | return rendered |
| 108 | |
| 109 | |
| 110 | def _main() -> None: |
| 111 | args = build_parser().parse_args() |
| 112 | max_bytes = bounded_file_limit(args.max_bytes) |
| 113 | manifest = None |
| 114 | if args.input is not None: |
| 115 | path = checked_input_file( |
| 116 | args.input, |
| 117 | root=args.root, |
| 118 | max_bytes=max_bytes, |
| 119 | ) |
| 120 | manifest = inspect_manifest( |
| 121 | path, |
| 122 | max_bytes=max_bytes, |
| 123 | include_sha256=False, |
| 124 | reveal_identifiers=args.reveal_identifiers, |
| 125 | ) |
| 126 | report = render_scaffold( |
| 127 | analysis_date=args.analysis_date, |
| 128 | manifest=manifest, |
| 129 | ) |
| 130 | emit_markdown( |
| 131 | report, |
| 132 | output=args.output, |
| 133 | root=args.root, |
| 134 | force=args.force, |
| 135 | ) |
| 136 | |
| 137 | |
| 138 | def main() -> int: |
| 139 | return run_cli(_main) |
| 140 | |
| 141 | |
| 142 | if __name__ == "__main__": |
| 143 | raise SystemExit(main()) |
文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。

