Skip to content

Commit e5a2e4b

Browse files
authored
Merge pull request #35 from QuantStrategyLab/codex/qar-publisher-period-2b1
QAR PR-2B1: validate candidates before publisher selection
2 parents 5a8d5cc + 0e24e37 commit e5a2e4b

6 files changed

Lines changed: 670 additions & 44 deletions

File tree

src/quant_advisor_research/archive_backfill.py

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing import Any
88

99
from .build_pipeline import DEFAULT_FEED_TITLE, DEFAULT_SITE_URL, copy_if_different
10-
from .publisher import publish_reports, unique_report_paths_by_content
10+
from .publisher import preflight_publish_destinations, publish_reports, require_publish_candidates
1111

1212

1313
REPORT_JSON_PATTERN = re.compile(r"advisory_report_\d{4}-\d{2}-\d{2}\.json")
@@ -24,29 +24,35 @@ def load_report(path: Path) -> dict[str, Any] | None:
2424

2525

2626
def discover_report_paths(artifact_roots: list[str | Path], explicit_reports: list[str | Path]) -> list[Path]:
27-
candidates: list[Path] = []
27+
explicit_candidates: list[Path] = []
2828
for report in explicit_reports:
2929
path = Path(report)
3030
if path.exists() and path.name.startswith("advisory_report_") and path.suffix == ".json":
31-
candidates.append(path)
31+
explicit_candidates.append(path)
32+
root_candidates: list[Path] = []
3233
for root in artifact_roots:
3334
root_path = Path(root)
3435
if not root_path.exists():
3536
continue
36-
candidates.extend(
37+
root_candidates.extend(
3738
path
3839
for path in root_path.rglob("advisory_report_*.json")
3940
if path.is_file() and REPORT_JSON_PATTERN.fullmatch(path.name)
4041
)
4142

42-
by_as_of: dict[str, Path] = {}
43-
for path in sorted(candidates, key=lambda item: (item.name, str(item))):
44-
payload = load_report(path)
45-
if not payload:
43+
unique_paths: list[Path] = []
44+
seen: set[Path] = set()
45+
all_candidates = explicit_candidates + root_candidates
46+
all_candidates.sort(key=lambda item: str(item))
47+
all_candidates.sort(key=lambda item: item.name, reverse=True)
48+
candidates = all_candidates
49+
for path in candidates:
50+
resolved = path.resolve()
51+
if resolved in seen:
4652
continue
47-
as_of = str(payload.get("as_of", ""))
48-
by_as_of[as_of] = path
49-
return [by_as_of[key] for key in sorted(by_as_of, reverse=True)]
53+
seen.add(resolved)
54+
unique_paths.append(path)
55+
return unique_paths
5056

5157

5258
def backfill_site_archive(
@@ -55,13 +61,23 @@ def backfill_site_archive(
5561
output_dir: str | Path,
5662
site_url: str,
5763
feed_title: str,
64+
current_report: str | Path | None = None,
5865
) -> list[Path]:
59-
if not report_paths:
66+
if not report_paths and current_report is None:
6067
raise ValueError("No advisory_report_YYYY-MM-DD.json files found for backfill.")
61-
report_paths = unique_report_paths_by_content(report_paths)
68+
selection = require_publish_candidates(current_report, report_paths)
69+
report_paths = list(selection.selected_paths)
70+
preflight_publish_destinations(report_paths)
6271
output = Path(output_dir)
6372
output.mkdir(parents=True, exist_ok=True)
64-
written = publish_reports(report_paths, output, site_url=site_url, feed_title=feed_title)
73+
written = publish_reports(
74+
report_paths,
75+
output,
76+
site_url=site_url,
77+
feed_title=feed_title,
78+
mandatory_current=current_report,
79+
recovered_history=report_paths if current_report is not None else None,
80+
)
6581
for report_path in report_paths:
6682
copy_if_different(report_path, output / report_path.name)
6783
markdown_path = report_path.with_suffix(".md")
@@ -77,6 +93,7 @@ def build_arg_parser() -> argparse.ArgumentParser:
7793
parser = argparse.ArgumentParser(description="Backfill advisory site archive from local workflow artifacts.")
7894
parser.add_argument("--artifact-root", action="append", default=[], help="Directory to recursively scan for advisory reports.")
7995
parser.add_argument("--report", action="append", default=[], help="Explicit advisory report JSON path.")
96+
parser.add_argument("--current-report", help="Optional explicit mandatory current report; --report remains history-only.")
8097
parser.add_argument("--output-dir", required=True, help="Static site output directory.")
8198
parser.add_argument("--site-url", default=DEFAULT_SITE_URL)
8299
parser.add_argument("--feed-title", default=DEFAULT_FEED_TITLE)
@@ -86,7 +103,13 @@ def build_arg_parser() -> argparse.ArgumentParser:
86103
def main(argv: list[str] | None = None) -> None:
87104
args = build_arg_parser().parse_args(argv)
88105
reports = discover_report_paths(args.artifact_root, args.report)
89-
backfill_site_archive(report_paths=reports, output_dir=args.output_dir, site_url=args.site_url, feed_title=args.feed_title)
106+
backfill_site_archive(
107+
report_paths=reports,
108+
current_report=args.current_report,
109+
output_dir=args.output_dir,
110+
site_url=args.site_url,
111+
feed_title=args.feed_title,
112+
)
90113
print(f"archive_backfill_reports={len(reports)} output={args.output_dir}")
91114

92115

src/quant_advisor_research/build_pipeline.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@
2121
write_market_confirmation_csv,
2222
)
2323
from .monthly_review import build_monthly_review, render_monthly_review_markdown
24-
from .publisher import publish_reports, unique_report_paths_by_content
24+
from .publisher import (
25+
preflight_publish_destinations,
26+
publish_reports,
27+
require_publish_candidates,
28+
unique_report_paths_by_content,
29+
)
2530
from .recommendation_review import build_recommendation_review, render_recommendation_review_markdown
2631

2732

@@ -285,7 +290,11 @@ def build_advisory_artifacts(
285290
report_paths = [report_json, *recovered_report_paths]
286291
else:
287292
report_paths = [report_json]
288-
report_paths = unique_report_paths_by_content(report_paths)
293+
if site_output:
294+
report_paths = list(require_publish_candidates(report_json, recovered_report_paths).selected_paths)
295+
preflight_publish_destinations(report_paths)
296+
else:
297+
report_paths = unique_report_paths_by_content(report_paths)
289298

290299
recommendation_review_json: Path | None = None
291300
recommendation_review_md: Path | None = None
@@ -304,7 +313,14 @@ def build_advisory_artifacts(
304313
write_text(recommendation_review_md, render_recommendation_review_markdown(review))
305314

306315
if site_output:
307-
publish_reports(report_paths, site_output, site_url=site_url, feed_title=feed_title)
316+
publish_reports(
317+
report_paths,
318+
site_output,
319+
site_url=site_url,
320+
feed_title=feed_title,
321+
mandatory_current=report_json,
322+
recovered_history=recovered_report_paths,
323+
)
308324
for path in report_paths:
309325
copy_if_different(Path(path), site_output / Path(path).name)
310326
copy_if_different(report_md, site_output / report_md.name)

0 commit comments

Comments
 (0)