Skills › Scientific Agent Skills › exploratory-data-analysis › report_scaffold.py
report_scaffold.py
143 lines · skills/exploratory-data-analysis/scripts/report_scaffold.py at 5f2bc03
1#!/usr/bin/env python3
2"""Generate a rigorous, redacted EDA Markdown report scaffold."""
4from __future__ import annotations
6import argparse
7from pathlib import Path
9from _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)
18from capability_manifest import inspect_manifest
21TEMPLATE = Path(__file__).resolve().parents[1] / "assets" / "report_template.md"
24def 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
69def render_scaffold(
70 *,
71 analysis_date: str,
72 manifest: dict | None,
73) -> str:
74 """Fill only trusted scalar placeholders in the bundled template."""
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
110def _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 )
138def main() -> int:
139 return run_cli(_main)
142if __name__ == "__main__":
143 raise SystemExit(main())