Skip to content

Commit 6de646f

Browse files
Pigbibicodex
andcommitted
fix: group publication variants by canonical period
Co-Authored-By: Codex <noreply@openai.com>
1 parent 132a957 commit 6de646f

2 files changed

Lines changed: 84 additions & 4 deletions

File tree

src/quant_advisor_research/publisher.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -368,18 +368,32 @@ def build_publication_plan(
368368
for index, path in enumerate(selected_paths)
369369
]
370370
valid_candidates = [candidate for candidate in candidates if candidate.report is not None]
371-
groups: dict[tuple[str, str], list[_ReportCandidate]] = {}
371+
groups: dict[str, list[_ReportCandidate]] = {}
372372
for candidate in valid_candidates:
373-
groups.setdefault((str(candidate.report["as_of"]), str(candidate.report["cadence"])), []).append(candidate)
373+
assert candidate.period is not None
374+
groups.setdefault(candidate.period.key, []).append(candidate)
374375
mandatory_resolved = Path(mandatory_current).resolve() if mandatory_current is not None else None
375376
entries_by_path: dict[Path, PublicationEntry] = {}
377+
ordered_groups: list[tuple[CanonicalPeriod, list[_ReportCandidate]]] = []
376378
for group in groups.values():
377379
ranked = _publication_rank(group)
378380
owner = next(
379381
(candidate for candidate in ranked if candidate.path.resolve() == mandatory_resolved),
380382
ranked[0],
381383
)
382-
for candidate in ranked:
384+
assert owner.period is not None
385+
ordered_groups.append(
386+
(owner.period, [owner, *(candidate for candidate in ranked if candidate is not owner)])
387+
)
388+
ordered_groups.sort(
389+
key=lambda item: (item[0].period_end, item[0].period_start, item[0].cadence),
390+
reverse=True,
391+
)
392+
ordered_candidates: list[_ReportCandidate] = []
393+
for _, group in ordered_groups:
394+
ordered_candidates.extend(group)
395+
owner = group[0]
396+
for candidate in group:
383397
assert candidate.report is not None and candidate.fingerprint is not None and candidate.generated_at is not None
384398
as_of = str(candidate.report["as_of"])
385399
canonical_html = report_filename(candidate.report)
@@ -405,7 +419,7 @@ def build_publication_plan(
405419
canonical_owner=canonical_owner,
406420
generated_at=candidate.generated_at,
407421
)
408-
entries = [entries_by_path[candidate.path] for candidate in _publication_rank(valid_candidates)]
422+
entries = [entries_by_path[candidate.path] for candidate in ordered_candidates]
409423
return PublicationPlan(tuple(entries))
410424

411425

tests/test_publisher_period_redesign.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from quant_advisor_research.advisory_report import build_advisory_report
1111
from quant_advisor_research.archive_backfill import discover_report_paths
1212
from quant_advisor_research.publisher import (
13+
build_publication_plan,
1314
classify_report_path,
1415
main as publisher_main,
1516
publish_reports,
@@ -83,6 +84,71 @@ def test_same_day_schema_precedes_generated_at(tmp_path: Path) -> None:
8384
assert unique_report_paths_by_content([v6_path, v5_path]) == [v6_path]
8485

8586

87+
@pytest.mark.parametrize(
88+
("cadence", "as_of_values"),
89+
[
90+
("weekly", ("2026-06-15", "2026-06-21")),
91+
("monthly", ("2026-06-01", "2026-06-30")),
92+
],
93+
)
94+
def test_publication_plan_groups_same_canonical_period_variants(
95+
tmp_path: Path, cadence: str, as_of_values: tuple[str, str]
96+
) -> None:
97+
first = build_v5(as_of_values[0])
98+
second = build_v5(as_of_values[1])
99+
first["cadence"] = second["cadence"] = cadence
100+
second["recommendations"][0]["reasons"] = ["different semantic content"]
101+
first_path = write_report(tmp_path / "first.json", first)
102+
second_path = write_report(tmp_path / "second.json", second)
103+
104+
plan = build_publication_plan([first_path, second_path])
105+
reversed_plan = build_publication_plan([second_path, first_path])
106+
107+
assert len(plan.entries) == 2
108+
assert sum(entry.canonical_owner for entry in plan.entries) == 1
109+
assert plan.entries[0].canonical_owner is True
110+
assert ".variant-" in plan.entries[1].json_name
111+
assert [(entry.json_name, entry.source_path.name) for entry in plan.entries] == [
112+
(entry.json_name, entry.source_path.name) for entry in reversed_plan.entries
113+
]
114+
115+
116+
def test_publication_plan_pins_mandatory_current_as_group_owner_and_first(
117+
tmp_path: Path,
118+
) -> None:
119+
current = build_v5("2026-06-21")
120+
recovered = build_v6("2026-06-15")
121+
recovered["summary"]["top_theme_ids"] = ["different-theme"]
122+
recovered["generated_at"] = "2026-06-16T12:00:00Z"
123+
recovered["expires_at"] = "2026-06-23T12:00:00Z"
124+
current_path = write_report(tmp_path / "advisory_report_2026-06-21.json", current)
125+
recovered_path = write_report(tmp_path / "recovered.json", recovered)
126+
127+
plan = build_publication_plan(
128+
[current_path], mandatory_current=current_path, recovered_history=[recovered_path]
129+
)
130+
131+
assert len(plan.entries) == 2
132+
assert plan.entries[0].source_path == current_path
133+
assert plan.entries[0].canonical_owner is True
134+
assert ".variant-" in plan.entries[1].json_name
135+
136+
output = tmp_path / "site"
137+
publish_reports(
138+
[current_path],
139+
output,
140+
site_url="https://example.invalid",
141+
feed_title="Test",
142+
mandatory_current=current_path,
143+
recovered_history=[recovered_path],
144+
)
145+
reports_index = json.loads((output / "reports_index.json").read_text(encoding="utf-8"))
146+
feed = (output / "feed.xml").read_text(encoding="utf-8")
147+
assert reports_index["reports"][0]["json"] == "advisory_report_2026-06-21.json"
148+
assert feed.index("2026-06-21") < feed.index("2026-06-15")
149+
assert "Tue, 16 Jun 2026 12:00:00 GMT" in feed
150+
151+
86152
def test_invalid_v6_cannot_shadow_valid_v5(tmp_path: Path) -> None:
87153
v5_path = write_report(tmp_path / "v5.json", build_v5())
88154
invalid_v6 = build_v6()

0 commit comments

Comments
 (0)