Skip to content

Commit c5cc335

Browse files
authored
feat(bundle): export review artifact bundle (#14)
1 parent 23a8ad6 commit c5cc335

5 files changed

Lines changed: 475 additions & 0 deletions

File tree

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,31 @@ pdf-quality-review-findings \
118118
--output /tmp/page_012_review_findings.md
119119
```
120120

121+
## Export Review Bundle
122+
123+
Export a static folder with the quality report, review findings, clean Markdown, chunk records, and chunk review
124+
Markdown:
125+
126+
```bash
127+
python -m pdf_quality_report.review_bundle \
128+
--input examples/mdpi_pdf_elements/page_012/normalized_blocks.json \
129+
--output-dir /tmp/page_012_pqr_review_bundle
130+
```
131+
132+
The command exits with `0` when all bundle files are written, even if the report decision is `REVIEW` or `BLOCK`.
133+
Input or output errors return `1`. See [Export Review Bundle](docs/how_to_export_review_bundle.md) for a short
134+
walkthrough.
135+
136+
Use `--out-dir` as a short alias for `--output-dir`.
137+
138+
If the package entry point is installed:
139+
140+
```bash
141+
pdf-quality-review-bundle \
142+
--input examples/mdpi_pdf_elements/page_012/normalized_blocks.json \
143+
--output-dir /tmp/page_012_pqr_review_bundle
144+
```
145+
121146
## Export Clean Markdown
122147

123148
Export normalized blocks to clean Markdown with source reference comments for downstream review or retrieval-oriented preparation:
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Export Review Bundle
2+
3+
Creates a static folder of existing PQR review artifacts from `normalized_blocks.json`.
4+
5+
Use it when you want one handoff folder for human review instead of running each export command separately.
6+
7+
## Command
8+
9+
```bash
10+
python -m pdf_quality_report.review_bundle \
11+
--input examples/mdpi_pdf_elements/page_012/normalized_blocks.json \
12+
--output-dir /tmp/page_012_pqr_review_bundle
13+
```
14+
15+
If the package entry point is installed:
16+
17+
```bash
18+
pdf-quality-review-bundle \
19+
--input examples/mdpi_pdf_elements/page_012/normalized_blocks.json \
20+
--output-dir /tmp/page_012_pqr_review_bundle
21+
```
22+
23+
`--out-dir` is available as a short alias for `--output-dir`.
24+
25+
## What The Bundle Contains
26+
27+
The output directory contains:
28+
29+
- `README.md`: bundle summary and file guide
30+
- `quality_report.md`: deterministic quality report with GO/REVIEW/BLOCK decision
31+
- `review_findings.md`: WARN/FAIL details with source context when available
32+
- `output.md`: clean Markdown export from normalized blocks
33+
- `chunk_records.jsonl`: provenance-preserving chunk records
34+
- `chunk_review.md`: human-reviewable Markdown view of chunk records
35+
36+
The command overwrites these known bundle files if they already exist. Unrelated files in the output directory are left
37+
untouched.
38+
39+
The bundle does not copy the source PDF or `normalized_blocks.json`.
40+
41+
## Exit Behavior
42+
43+
The command exits with `0` when all bundle files are written, even if the quality-report decision is `REVIEW` or
44+
`BLOCK`. Input or output errors return `1`.
45+
46+
## What This Does Not Do
47+
48+
- It does not add new parser-output checks.
49+
- It does not approve, correct, or remove blocks.
50+
- It does not create a ZIP file or manifest schema.
51+
- It is not a workflow system, task tracker, dashboard, or approval process.
52+
- It does not validate OCR accuracy, table reconstruction, parser correctness, or downstream readiness.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pdf-quality-markdown = "pdf_quality_report.to_markdown:main"
3030
pdf-quality-chunk = "pdf_quality_report.chunk:main"
3131
pdf-quality-chunk-review = "pdf_quality_report.chunk_review:main"
3232
pdf-quality-review-findings = "pdf_quality_report.review_findings:main"
33+
pdf-quality-review-bundle = "pdf_quality_report.review_bundle:main"
3334

3435
[tool.pytest.ini_options]
3536
testpaths = ["tests"]
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
"""Export static review bundles from normalized block JSON."""
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import json
7+
import sys
8+
from collections.abc import Sequence
9+
from dataclasses import dataclass
10+
from pathlib import Path
11+
12+
from .checks import run_quality_checks
13+
from .chunk import (
14+
ChunkExportError,
15+
ChunkRecord,
16+
build_chunk_records,
17+
load_normalized_blocks,
18+
)
19+
from .chunk_review import render_chunk_review_markdown
20+
from .report import load_uif_document, render_markdown_report
21+
from .review_findings import render_review_findings_markdown
22+
from .to_markdown import convert_uif_to_markdown
23+
24+
BUNDLE_FILES = (
25+
"README.md",
26+
"quality_report.md",
27+
"review_findings.md",
28+
"output.md",
29+
"chunk_records.jsonl",
30+
"chunk_review.md",
31+
)
32+
33+
34+
class ReviewBundleExportError(Exception):
35+
"""Raised when a review bundle cannot be exported."""
36+
37+
38+
@dataclass(frozen=True)
39+
class ReviewBundleResult:
40+
"""Result metadata for a written review bundle."""
41+
42+
output_dir: Path
43+
files: tuple[str, ...]
44+
decision: str
45+
total_blocks: int
46+
warnings: int
47+
hard_failures: int
48+
49+
50+
def export_review_bundle(input_path: Path, output_dir: Path) -> ReviewBundleResult:
51+
"""Build and write all static review bundle artifacts.
52+
53+
Args:
54+
input_path: Path to a normalized blocks JSON file.
55+
output_dir: Directory where bundle files should be written.
56+
57+
Returns:
58+
Result metadata for the written bundle.
59+
"""
60+
if output_dir.exists() and not output_dir.is_dir():
61+
raise ReviewBundleExportError("output directory path exists and is not a directory")
62+
63+
bundle = _build_bundle(input_path)
64+
try:
65+
output_dir.mkdir(parents=True, exist_ok=True)
66+
for filename, content in bundle.files.items():
67+
(output_dir / filename).write_text(content, encoding="utf-8")
68+
except OSError as exc:
69+
raise ReviewBundleExportError(f"could not write bundle output: {exc}") from exc
70+
71+
return ReviewBundleResult(
72+
output_dir=output_dir,
73+
files=tuple(bundle.files),
74+
decision=bundle.decision,
75+
total_blocks=bundle.total_blocks,
76+
warnings=bundle.warnings,
77+
hard_failures=bundle.hard_failures,
78+
)
79+
80+
81+
def main(argv: Sequence[str] | None = None) -> int:
82+
"""CLI entrypoint for review bundle export."""
83+
parser = argparse.ArgumentParser(
84+
description="Export a static PQR review bundle from normalized block JSON.",
85+
)
86+
parser.add_argument(
87+
"--input",
88+
dest="input_path",
89+
required=True,
90+
type=Path,
91+
help="Path to normalized block JSON.",
92+
)
93+
parser.add_argument(
94+
"--output-dir",
95+
"--out-dir",
96+
dest="output_dir",
97+
required=True,
98+
type=Path,
99+
help="Directory where review bundle files should be written.",
100+
)
101+
args = parser.parse_args(argv)
102+
103+
try:
104+
export_review_bundle(args.input_path, args.output_dir)
105+
except (ReviewBundleExportError, ChunkExportError, OSError, ValueError) as exc:
106+
print(f"Error: {exc}", file=sys.stderr)
107+
return 1
108+
return 0
109+
110+
111+
@dataclass(frozen=True)
112+
class _BundleContent:
113+
files: dict[str, str]
114+
decision: str
115+
total_blocks: int
116+
warnings: int
117+
hard_failures: int
118+
119+
120+
def _build_bundle(input_path: Path) -> _BundleContent:
121+
document = load_uif_document(input_path)
122+
normalized_document = load_normalized_blocks(input_path)
123+
report = run_quality_checks(document)
124+
chunk_records = build_chunk_records(normalized_document)
125+
chunk_jsonl = _chunk_records_jsonl(chunk_records)
126+
payloads = {
127+
"README.md": _bundle_readme(
128+
total_blocks=report.total_blocks,
129+
warnings=report.warnings,
130+
hard_failures=report.hard_failures,
131+
decision=report.decision,
132+
chunk_count=len(chunk_records),
133+
),
134+
"quality_report.md": render_markdown_report(report),
135+
"review_findings.md": render_review_findings_markdown(report, normalized_document.blocks),
136+
"output.md": convert_uif_to_markdown(document),
137+
"chunk_records.jsonl": chunk_jsonl,
138+
"chunk_review.md": render_chunk_review_markdown(chunk_records),
139+
}
140+
files = {filename: payloads[filename] for filename in BUNDLE_FILES}
141+
return _BundleContent(
142+
files=files,
143+
decision=report.decision,
144+
total_blocks=report.total_blocks,
145+
warnings=report.warnings,
146+
hard_failures=report.hard_failures,
147+
)
148+
149+
150+
def _chunk_records_jsonl(chunk_records: Sequence[ChunkRecord]) -> str:
151+
lines = [json.dumps(record.to_json(), ensure_ascii=False) for record in chunk_records]
152+
text = "\n".join(lines)
153+
return f"{text}\n" if text else ""
154+
155+
156+
def _bundle_readme(
157+
*,
158+
total_blocks: int,
159+
warnings: int,
160+
hard_failures: int,
161+
decision: str,
162+
chunk_count: int,
163+
) -> str:
164+
lines = [
165+
"# PQR Review Bundle",
166+
"",
167+
"This is a static review bundle generated from normalized parser output.",
168+
"",
169+
"## Summary",
170+
"",
171+
f"- decision: {decision}",
172+
f"- total_blocks: {total_blocks}",
173+
f"- warnings: {warnings}",
174+
f"- hard_failures: {hard_failures}",
175+
f"- chunks: {chunk_count}",
176+
"",
177+
"## Files",
178+
"",
179+
"- `quality_report.md`: deterministic quality report with GO/REVIEW/BLOCK decision.",
180+
"- `review_findings.md`: WARN/FAIL details with source context when available.",
181+
"- `output.md`: clean Markdown export from normalized blocks.",
182+
"- `chunk_records.jsonl`: provenance-preserving chunk records.",
183+
"- `chunk_review.md`: human-reviewable Markdown view of chunk records.",
184+
"",
185+
"## Boundaries",
186+
"",
187+
"- This bundle does not approve, correct, or remove parser-output blocks.",
188+
"- This bundle is not a workflow system, task tracker, dashboard, ZIP package, or manifest schema.",
189+
"- This bundle does not validate OCR accuracy, table reconstruction, parser correctness, or downstream "
190+
"readiness.",
191+
"- The export exits successfully when bundle files are written, even if the report decision is REVIEW or "
192+
"BLOCK.",
193+
"",
194+
]
195+
return "\n".join(lines)
196+
197+
198+
if __name__ == "__main__":
199+
raise SystemExit(main())

0 commit comments

Comments
 (0)