Skill Tracer
Skills › Scientific Agent Skills › scientific-writing › scaffold_manuscript.py

scaffold_manuscript.py

143 lines · skills/scientific-writing/scripts/scaffold_manuscript.py at 5f2bc03

1"""Generate a local, explicitly incomplete manuscript workspace."""
2
3from __future__ import annotations
4
5import argparse
6import json
7import re
8from pathlib import Path
9from typing import Any
11from _common import (
12 InputError,
13 emit_report,
14 read_json,
15 read_text,
16 require_object,
17 run,
18 write_new_text,
21TOOL = "scaffold_manuscript"
22ASSET_DIR = Path(__file__).resolve().parents[1] / "assets"
23DOCUMENT_ID_RE = re.compile(r"^[A-Za-z][A-Za-z0-9._-]{1,63}$")
24GUIDELINE_ID_RE = re.compile(r"^[a-z][a-z0-9.-]{1,63}$")
25TEMPLATE_FILES = {
26 "manuscript.md": "manuscript_scaffold.md",
27 "claims.csv": "claim_evidence_template.csv",
28 "source_manifest.json": "source_manifest_template.json",
29 "consistency_manifest.json": "consistency_manifest_template.json",
30 "authorship.json": "authorship_template.json",
34def _json_text(value: Any) -> str:
35 return json.dumps(value, indent=2, sort_keys=True) + "\n"
38def _prepare_manifest(
39 *,
40 document_id: str,
41 study_design: str,
42 guidelines: list[str],
43) -> str:
44 data = require_object(
45 read_json(ASSET_DIR / "manuscript_manifest_template.json"),
46 "manuscript_manifest_template",
47 )
48 data["document_id"] = document_id
49 data["study_design"] = study_design
50 data["reporting_guidelines"] = guidelines
51 return _json_text(data)
54def _prepare_coverage(guidelines: list[str]) -> str:
55 data = require_object(
56 read_json(ASSET_DIR / "reporting_coverage_template.json"),
57 "reporting_coverage_template",
58 )
59 data["guideline_id"] = guidelines[0] if guidelines else "[[TODO:guideline-id]]"
60 return _json_text(data)
63def generate(
64 output_dir: Path,
65 *,
66 document_id: str,
67 study_design: str,
68 guidelines: list[str],
69) -> list[str]:
70 if output_dir.exists() or output_dir.is_symlink():
71 raise InputError("output directory already exists; refusing to overwrite")
72 parent = output_dir.parent
73 if not parent.is_dir() or parent.is_symlink():
74 raise InputError("output parent must be an existing regular directory")
76 prepared: dict[str, str] = {
77 destination: read_text(ASSET_DIR / source, {Path(source).suffix})
78 for destination, source in TEMPLATE_FILES.items()
79 }
80 prepared["manuscript_manifest.json"] = _prepare_manifest(
81 document_id=document_id,
82 study_design=study_design,
83 guidelines=guidelines,
84 )
85 prepared["reporting_coverage.json"] = _prepare_coverage(guidelines)
87 output_dir.mkdir(mode=0o700)
88 for filename in sorted(prepared):
89 write_new_text(
90 output_dir / filename,
91 prepared[filename],
92 {Path(filename).suffix},
93 )
94 return sorted(prepared)
97def build_parser() -> argparse.ArgumentParser:
98 parser = argparse.ArgumentParser(
99 description=(
100 "Create a deterministic local manuscript workspace. Generated files "
101 "are marked incomplete and must pass all validators before submission."
102 )
103 )
104 parser.add_argument("--output-dir", required=True, help="new directory to create")
105 parser.add_argument("--document-id", required=True)
106 parser.add_argument("--study-design", required=True)
107 parser.add_argument(
108 "--guideline",
109 action="append",
110 default=[],
111 help="candidate reporting-guideline ID; repeat as needed",
112 )
113 return parser
116def cli() -> int:
117 args = build_parser().parse_args()
118 if not DOCUMENT_ID_RE.fullmatch(args.document_id):
119 raise InputError("document-id must be 2-64 safe identifier characters")
120 if not DOCUMENT_ID_RE.fullmatch(args.study_design):
121 raise InputError("study-design must be a safe identifier")
122 guidelines = sorted(set(args.guideline))
123 if any(not GUIDELINE_ID_RE.fullmatch(value) for value in guidelines):
124 raise InputError("guideline IDs must be safe lowercase identifiers")
125 files = generate(
126 Path(args.output_dir),
127 document_id=args.document_id,
128 study_design=args.study_design,
129 guidelines=guidelines,
130 )
131 return emit_report(
132 TOOL,
133 [],
134 summary={
135 "files_created": files,
136 "submission_ready": False,
137 "overwrites": False,
138 },
139 )
142if __name__ == "__main__":
143 run(TOOL, cli)