Черновик ПЗ

This commit is contained in:
2026-05-25 19:34:22 +03:00
parent adc3730b8d
commit 2b139a18b3
72 changed files with 3570 additions and 0 deletions

7
Report/scripts/build-pz.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
python3 scripts/gen_listings.py
python3 scripts/gen_test_tables.py
python3 scripts/check_images.py
typst compile --root .. ояснительная_записка_ПытковРЕ.typ"

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Verify IMAGES_REGISTRY.md entries exist on disk and match .typ references."""
from __future__ import annotations
import re
import sys
from pathlib import Path
def main() -> int:
report = Path(__file__).resolve().parent.parent
repo = report.parent
images = report / "images"
registry = images / "IMAGES_REGISTRY.md"
typ = report / "Пояснительная_записка_ПытковРЕ.typ"
errors = 0
if not registry.exists():
print("Missing IMAGES_REGISTRY.md", file=sys.stderr)
return 1
rows = re.findall(
r"\|\s*\d+\s*\|\s*([^\|]+?)\s*\|\s*(\w+)\s*\|",
registry.read_text(encoding="utf-8"),
)
rows = [(f.strip(), s.strip()) for f, s in rows if f.startswith("fig_")]
for fname, status in rows:
p = images / fname
if not p.exists():
print(f"MISSING file: {fname} (status {status})", file=sys.stderr)
errors += 1
elif status == "ready" and p.stat().st_size < 1000:
print(f"WARN small file: {fname}", file=sys.stderr)
if typ.exists():
text = typ.read_text(encoding="utf-8")
for m in re.finditer(r'image\("(?:Report/)?images/([^"]+)"', text):
f = images / m.group(1)
if not f.exists():
print(f"MISSING in typ: images/{m.group(1)}", file=sys.stderr)
errors += 1
if errors:
print(f"check_images: {errors} error(s)", file=sys.stderr)
return 1
print("check_images: OK", file=sys.stderr)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,196 @@
#!/usr/bin/env python3
"""Generate Typst listing wrappers for Appendix A from Wallenc source tree."""
from __future__ import annotations
import argparse
import hashlib
import sys
from pathlib import Path
try:
import yaml
except ImportError:
yaml = None # type: ignore
def load_config(path: Path) -> dict:
text = path.read_text(encoding="utf-8")
if yaml is None:
raise SystemExit("PyYAML required: pip install pyyaml")
return yaml.safe_load(text)
def path_hash(rel: str) -> str:
return hashlib.sha256(rel.encode()).hexdigest()[:8]
def lang_for(path: Path, ext_map: dict, name_map: dict) -> str:
if path.name in name_map:
return name_map[path.name]
return ext_map.get(path.suffix.lower(), "text")
def should_skip(path: Path, parts: tuple[str, ...], cfg: dict) -> bool:
exclude_dirs = set(cfg.get("exclude_dirs", []))
if any(p in exclude_dirs for p in parts):
return True
s = path.as_posix()
for g in cfg.get("exclude_globs", []):
g = g.replace("**/", "")
if g.startswith("**/"):
g = g[3:]
if g.endswith("/**"):
if g[:-3] in parts:
return True
elif g.startswith("*."):
if s.endswith(g[1:]):
return True
elif g in s:
return True
return False
def collect_files(scan_root: Path, cfg: dict) -> list[Path]:
ext_map: dict = cfg.get("include_extensions", {})
name_map: dict = cfg.get("include_filenames", {})
allowed_suffixes = set(ext_map.keys())
allowed_names = set(name_map.keys())
files: list[Path] = []
for p in scan_root.rglob("*"):
if not p.is_file():
continue
rel_parts = p.relative_to(scan_root).parts
if should_skip(p, rel_parts, cfg):
continue
if p.name in allowed_names or p.suffix.lower() in allowed_suffixes:
files.append(p)
return sorted(files, key=lambda x: x.as_posix())
def module_key(rel: Path, cfg: dict) -> str:
order = cfg.get("modules_order", [])
parts = rel.parts
if rel.name in {Path(f).name for f in cfg.get("root_build_files", [])}:
return "root"
if parts and parts[0] in order:
return parts[0]
return "other"
def group_files(files: list[Path], scan_root: Path, cfg: dict) -> dict[str, list[Path]]:
groups: dict[str, list[Path]] = {}
root_files = {Path(f).name for f in cfg.get("root_build_files", [])}
for f in files:
rel = f.relative_to(scan_root)
if rel.name in root_files or len(rel.parts) == 1 and rel.suffix in {".kts", ".properties"}:
groups.setdefault("root", []).append(f)
continue
key = module_key(rel, cfg)
groups.setdefault(key, []).append(f)
order = cfg.get("modules_order", []) + ["other"]
return {k: groups[k] for k in order if k in groups}
def typst_escape_path(rel: str) -> str:
return rel.replace("\\", "/")
def write_listing(
out_path: Path,
rel_from_report: str,
caption: str,
label: str,
lang: str,
) -> None:
rel_typ = typst_escape_path(rel_from_report)
# Подпись перед кодом: figure.caption(position: top) в теле (placement: top — про float, не порядок).
content = (
f'#let lst-body-{label} = read("{rel_typ}")\n'
f"#figure(\n"
f" [\n"
f" #figure.caption(position: top)[{caption}]\n"
f" #block(breakable: true)[\n"
f" #raw(lst-body-{label}, lang: \"{lang}\", block: true)\n"
f" ]\n"
f" ],\n"
f" supplement: [Листинг],\n"
f" gap: 0.4em,\n"
f") <lst-{label}>\n\n"
)
out_path.write_text(content, encoding="utf-8")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--config", default=None)
args = parser.parse_args()
script_dir = Path(__file__).resolve().parent
report_dir = script_dir.parent
config_path = Path(args.config) if args.config else report_dir / "listings" / "listings.config.yaml"
cfg = load_config(config_path)
scan_root = (report_dir / cfg.get("scan_root", "..")).resolve()
generated = report_dir / "listings" / "generated"
ext_map: dict = cfg.get("include_extensions", {})
name_map: dict = cfg.get("include_filenames", {})
files = collect_files(scan_root, cfg)
if args.dry_run:
for f in files:
print(f.relative_to(scan_root).as_posix())
print(f"Total: {len(files)}", file=sys.stderr)
return 0
generated.mkdir(parents=True, exist_ok=True)
groups = group_files(files, scan_root, cfg)
listing_paths: list[str] = []
module_includes: list[str] = []
for mod, mod_files in groups.items():
mod_listings: list[str] = []
for f in mod_files:
rel_repo = f.relative_to(scan_root).as_posix()
# Paths relative to listings/generated/*.typ (typst compile --root ..)
rel_report = Path("../../..") / rel_repo
rel_report_str = rel_report.as_posix()
hid = path_hash(rel_repo)
label = hid
cap = f"Исходный файл `{rel_repo}`"
lang = lang_for(f, ext_map, name_map)
listing_name = f"listing-{hid}.typ"
listing_path = generated / listing_name
write_listing(listing_path, rel_report_str, cap, label, lang)
mod_listings.append(listing_name)
listing_paths.append(listing_name)
mod_file = generated / f"module-{mod}.typ"
with mod_file.open("w", encoding="utf-8") as mf:
title = "Система сборки" if mod == "root" else f"Модуль :{mod}"
mf.write(f"== {title}\n\n")
if cfg.get("pagebreak_per_module") and mod != "root":
mf.write("#pagebreak(weak: true)\n\n")
for ln in mod_listings:
mf.write(f'#include "{ln}"\n')
module_includes.append(f"module-{mod}.typ")
index = generated / "_index.typ"
with index.open("w", encoding="utf-8") as ix:
ix.write(f"// Generated listings: {len(files)} files\n\n")
appendix = generated / "appendix-a.typ"
with appendix.open("w", encoding="utf-8") as ap:
ap.write("// AUTO-GENERATED by gen_listings.py — do not edit\n\n")
ap.write("#set figure(gap: 0.4em)\n\n")
for mi in module_includes:
ap.write(f'#include "{mi}"\n')
print(f"Generated {len(files)} listings in {generated}", file=sys.stderr)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""Generate Typst tables of unit tests for chapter 5."""
from __future__ import annotations
import re
from collections import defaultdict
from pathlib import Path
def typst_escape(s: str) -> str:
return s.replace("\\", "\\\\").replace("`", "\\`")
def emit_table(
lines: list[str],
caption: str,
ncol: int,
headers: list[str],
data_rows: list[list[str]],
label: str,
) -> None:
lines.append("#pz-test-table(\n")
lines.append(f" [{caption}],\n")
lines.append(f" {ncol},\n")
lines.append(" table.header(\n")
for h in headers:
lines.append(f" [{typst_escape(h)}],\n")
lines.append(" ),\n")
for row in data_rows:
cells = ", ".join(f"[{typst_escape(c)}]" for c in row)
lines.append(f" {cells},\n")
lines.append(f") <{label}>\n\n")
def main() -> None:
root = Path(__file__).resolve().parents[2]
out = Path(__file__).resolve().parents[1] / "includes" / "ch05-tests-generated.typ"
rows: list[tuple[str, str, str, str]] = []
for p in sorted(root.rglob("*.kt")):
if "/src/test/" not in p.as_posix():
continue
text = p.read_text(encoding="utf-8", errors="replace")
cls_m = re.search(r"class\s+(\w+)", text)
if not cls_m:
continue
cls = cls_m.group(1)
mod = p.parts[p.parts.index("src") - 1]
rel = p.relative_to(root).as_posix()
for m in re.finditer(r"@Test[\s\S]*?fun\s+(?:`([^`]+)`|(\w+))\s*\(", text):
name = m.group(1) or m.group(2)
rows.append((mod, cls, name, rel))
by_mod: dict[str, list] = defaultdict(list)
for mod, cls, name, rel in rows:
by_mod[mod].append((cls, name, rel))
lines = [
"// AUTO-GENERATED by gen_test_tables.py — include from ch05.typ\n",
'#import "common.typ": pz-test-table\n\n',
]
summary_rows = []
for mod in sorted(by_mod):
for cls, name, rel in by_mod[mod]:
short = rel.split("/")[-1]
summary_rows.append([mod, cls, name, short])
emit_table(
lines,
"Сводка модульных unit-тестов (src/test)",
4,
["Модуль", "Класс", "Метод", "Файл"],
summary_rows,
"tbl-unit-all",
)
for mod in sorted(by_mod):
safe = mod.replace("-", "_")
lines.append(f"=== Реестр тестов модуля :{mod}\n\n")
mod_rows = [
[cls, name, rel.split("/")[-1]]
for cls, name, rel in by_mod[mod]
]
emit_table(
lines,
f"Unit-тесты модуля :{mod}",
3,
["Класс", "Метод", "Файл"],
mod_rows,
f"tbl-unit-{safe}",
)
out.write_text("".join(lines), encoding="utf-8")
print(f"Wrote {out} ({len(rows)} tests)", flush=True)
if __name__ == "__main__":
main()