main
py 1,049 lines 42.9 KB
Raw
1 import json
2 import tempfile
3 import unittest
4 from pathlib import Path
5 from unittest import mock
6
7 import scripts.analysis_gate as analysis_gate
8 from scripts.render_press_context import NO_PRESS_SENTINEL, press_token_estimate
9
10 RAW_PAYLOAD = {"week": "2026-W23"}
11 RAW_PAYLOAD_WITH_REPOS = {
12 "week": "2026-W23",
13 "crawled_at": "2026-06-01T00:00:00Z",
14 "new_repos": [{"full_name": "owner/repo", "stars": 1000}],
15 "trending_repos": [
16 {"full_name": "owner/repo-a", "stars": 300},
17 {"full_name": "owner/repo-b", "stars": 200},
18 ],
19 }
20 CURRENT_DATETIME = "2026-06-01T00:00:00Z"
21
22
23 def make_body(
24 *, alternate_heading: str = "## Where Industry Meets Code", include_todo_app: bool = False
25 ) -> str:
26 trends = " ".join(
27 [
28 "This section names the macro trends of the week, explaining what is driving each pattern and why it matters to practitioners tracking real engineering movement."
29 ]
30 * 4
31 )
32 industry = " ".join(
33 [
34 "Developer activity and press coverage aligned around practical tooling, but the narrative reveals where media attention diverged from what engineers are actually building."
35 ]
36 * 3
37 )
38 signal_noise = " ".join(
39 [
40 "The durable pattern is disciplined infrastructure work and credible developer experience improvements. The weak pattern is wrapper churn, shallow agent branding, and launches that borrow attention without demonstrating technical substance or ecosystem fit."
41 ]
42 * 3
43 )
44 blind_spots = " ".join(
45 [
46 "What is missing is more progress on observability, testing ergonomics, and dependable security tooling for smaller teams that still need production discipline."
47 ]
48 * 3
49 )
50 week_ahead = " ".join(
51 [
52 "The week matters because it shows teams rewarding grounded software that reduces toil, while hype-heavy experiments still struggle to prove lasting value."
53 ]
54 * 2
55 )
56 if include_todo_app:
57 week_ahead += " Several repositories mention todo apps as legitimate examples rather than placeholder notes."
58 return f"""
59 ## This Week's Trends
60
61 {trends}
62
63 {alternate_heading}
64
65 {industry}
66
67 ## Signal & Noise
68
69 {signal_noise}
70
71 ## Blind Spots
72
73 {blind_spots}
74
75 ## The Week Ahead
76
77 {week_ahead}
78
79 ## Key References
80
81 ### Notable Projects
82
83 - [owner/repo-a](https://github.com/owner/repo-a) — anchors the automation trend with practical defaults.
84 - [owner/repo-b](https://github.com/owner/repo-b) — observability tooling for smaller teams.
85
86 ### Press & Industry
87
88 No press data was provided this week.
89 """.strip()
90
91
92 def make_analysis(frontmatter: str, body: str) -> str:
93 return f"---\n{frontmatter}\n---\n\n{body}\n"
94
95
96 VALID_FRONTMATTER = '''title: "The Week Local Models Went Mainstream"
97 date: 2026-06-01T00:00:00Z
98 week: 2026-W23
99 year: 2026
100 tags:
101 - ai
102 - agents
103 - infrastructure
104 categories:
105 - weekly
106 repos_featured: 9
107 stars_tracked: 1200
108 top_repo: owner/repo
109 quality_score: 82
110 summary: "A grounded week focused on practical tools."'''.strip()
111
112
113 class AnalysisGateTests(unittest.TestCase):
114 def test_objective_quality_press_included_never_scores_below_press_less(self) -> None:
115 body = make_body().replace(
116 "No press data was provided this week.",
117 "- [Industry report](https://example.com/industry-report) — confirms the trend.",
118 )
119 analysis = make_analysis(VALID_FRONTMATTER, body)
120
121 press_score, press_breakdown = analysis_gate.compute_objective_quality(
122 analysis, RAW_PAYLOAD_WITH_REPOS, press_context_available=True
123 )
124 press_less_score, press_less_breakdown = analysis_gate.compute_objective_quality(
125 analysis, RAW_PAYLOAD_WITH_REPOS, press_context_available=False
126 )
127
128 # jmservera/SquadScope#583: prevent the W30 paradox where adding real press
129 # evidence made an otherwise identical summary score lower.
130 self.assertGreaterEqual(press_score, press_less_score)
131 self.assertGreater(press_score, press_less_score)
132 self.assertEqual(press_breakdown["words"], press_less_breakdown["words"])
133 self.assertEqual(press_breakdown["repo_citations"], press_less_breakdown["repo_citations"])
134 self.assertEqual(press_breakdown["press_citations"], 1)
135 self.assertEqual(press_less_breakdown["press"], 0)
136
137 def test_objective_quality_no_press_week_retains_publishable_base_score(self) -> None:
138 score, breakdown = analysis_gate.compute_objective_quality(
139 make_analysis("week: 2026-W23", "A short press-less summary."),
140 RAW_PAYLOAD,
141 press_context_available=False,
142 )
143
144 self.assertGreaterEqual(score, 60)
145 self.assertEqual(breakdown["base"], 60)
146 self.assertEqual(breakdown["press"], 0)
147 self.assertFalse(breakdown["press_available"])
148
149 def test_objective_quality_depth_and_evidence_scale_and_cap(self) -> None:
150 short_text = make_analysis("week: 2026-W23", "word " * 200)
151 medium_text = make_analysis("week: 2026-W23", "word " * 700)
152 long_text = make_analysis("week: 2026-W23", "word " * 1400)
153
154 _, short = analysis_gate.compute_objective_quality(short_text, {}, False)
155 _, medium = analysis_gate.compute_objective_quality(medium_text, {}, False)
156 _, long = analysis_gate.compute_objective_quality(long_text, {}, False)
157
158 self.assertEqual(short["depth"], 0)
159 self.assertGreater(medium["depth"], short["depth"])
160 self.assertEqual(long["depth"], 15)
161
162 repos = [f"owner/repo-{index}" for index in range(12)]
163 raw_payload = {
164 "new_repos": [{"full_name": repo} for repo in repos[:6]],
165 "trending_repos": [{"full_name": repo} for repo in repos[6:]],
166 }
167
168 def cited_analysis(count: int) -> str:
169 links = "\n".join(f"- [{repo}](https://github.com/{repo})" for repo in repos[:count])
170 return make_analysis("week: 2026-W23", links)
171
172 _, none = analysis_gate.compute_objective_quality(cited_analysis(0), raw_payload, False)
173 _, half = analysis_gate.compute_objective_quality(cited_analysis(5), raw_payload, False)
174 _, full = analysis_gate.compute_objective_quality(cited_analysis(12), raw_payload, False)
175 _, no_inventory = analysis_gate.compute_objective_quality(cited_analysis(1), {}, False)
176
177 self.assertEqual(none["evidence"], 0)
178 self.assertEqual(half["evidence"], 5)
179 self.assertEqual(full["evidence"], 10)
180 self.assertEqual(full["repo_citations"], 12)
181 self.assertEqual(no_inventory["evidence"], 0)
182 self.assertEqual(no_inventory["repo_citations"], 0)
183
184 def test_objective_quality_press_bonus_caps_and_requires_press_context(self) -> None:
185 body = """## Key References
186
187 ### Notable Projects
188
189 - [owner/repo-a](https://github.com/owner/repo-a)
190
191 ### Press & Industry
192
193 - [One](https://one.example/article)
194 - [Two](https://two.example/article)
195 - [Three](https://three.example/article)
196 - [Four](https://four.example/article)
197 - [GitHub](https://github.com/owner/repo-a)
198 """
199 analysis = make_analysis("week: 2026-W23", body)
200
201 _, available = analysis_gate.compute_objective_quality(
202 analysis, RAW_PAYLOAD_WITH_REPOS, True
203 )
204 _, unavailable = analysis_gate.compute_objective_quality(
205 analysis, RAW_PAYLOAD_WITH_REPOS, False
206 )
207
208 self.assertEqual(available["press"], 15)
209 self.assertEqual(available["press_citations"], 4)
210 self.assertEqual(unavailable["press"], 0)
211 self.assertEqual(unavailable["press_citations"], 0)
212
213 def test_objective_quality_excludes_github_owned_hosts_from_press(self) -> None:
214 body = """## Key References
215
216 ### Notable Projects
217
218 - [owner/repo-a](https://github.com/owner/repo-a)
219
220 ### Press & Industry
221
222 - [Real press](https://press.example/article)
223 - [Gist](https://gist.github.com/owner/abc123)
224 - [Raw](https://raw.githubusercontent.com/owner/repo-a/main/README.md)
225 - [Sub](https://api.github.com/repos/owner/repo-a)
226 """
227 analysis = make_analysis("week: 2026-W23", body)
228
229 _, breakdown = analysis_gate.compute_objective_quality(
230 analysis, RAW_PAYLOAD_WITH_REPOS, True
231 )
232
233 self.assertEqual(breakdown["press_citations"], 1)
234
235 def test_objective_quality_ignores_urls_after_press_subsection(self) -> None:
236 body = """## Key References
237
238 ### Press & Industry
239
240 - [Real press](https://press.example/article)
241
242 ### Further Reading
243
244 - [Not press](https://blog.example/post)
245 - [Also not](https://docs.example/guide)
246 """
247 analysis = make_analysis("week: 2026-W23", body)
248
249 _, breakdown = analysis_gate.compute_objective_quality(
250 analysis, RAW_PAYLOAD_WITH_REPOS, True
251 )
252
253 self.assertEqual(breakdown["press_citations"], 1)
254
255 def test_set_frontmatter_quality_score_replaces_inserts_and_preserves_body(self) -> None:
256 body = "Body with quality_score: 999 that must remain untouched.\n"
257 existing = make_analysis("week: 2026-W23\nquality_score: 99", body)
258 missing = make_analysis("week: 2026-W23", body)
259
260 replaced = analysis_gate.set_frontmatter_quality_score(existing, 72)
261 inserted = analysis_gate.set_frontmatter_quality_score(missing, 68)
262
263 self.assertIn("\nquality_score: 72\n---", replaced)
264 self.assertEqual(replaced.split("---\n", 2)[2], f"\n{body}\n")
265 self.assertIn("\nquality_score: 68\n---", inserted)
266 self.assertEqual(inserted.split("---\n", 2)[2], f"\n{body}\n")
267 with self.assertRaisesRegex(ValueError, "missing YAML frontmatter"):
268 analysis_gate.set_frontmatter_quality_score(body, 70)
269
270 def test_main_overwrites_llm_quality_score_and_reports_objective_breakdown(self) -> None:
271 tests_root = Path(__file__).resolve().parent
272 with tempfile.TemporaryDirectory(dir=tests_root) as tmpdir:
273 workspace = Path(tmpdir)
274 analysis_path = workspace / "candidate.md"
275 raw_path = workspace / "raw.json"
276 report_path = workspace / "report.json"
277 original = make_analysis(
278 VALID_FRONTMATTER.replace("quality_score: 82", "quality_score: 99"),
279 make_body(),
280 )
281 expected_score, expected_breakdown = analysis_gate.compute_objective_quality(
282 original, RAW_PAYLOAD_WITH_REPOS, False
283 )
284 analysis_path.write_text(original, encoding="utf-8")
285 raw_path.write_text(json.dumps(RAW_PAYLOAD_WITH_REPOS), encoding="utf-8")
286
287 self.assertEqual(
288 analysis_gate.main(
289 [
290 "--analysis-file",
291 str(analysis_path),
292 "--raw-json",
293 str(raw_path),
294 "--current-datetime",
295 CURRENT_DATETIME,
296 "--source",
297 "copilot-cli",
298 "--model",
299 "copilot-default",
300 "--report-json",
301 str(report_path),
302 ]
303 ),
304 0,
305 )
306
307 frontmatter, _ = analysis_gate.extract_frontmatter(
308 analysis_path.read_text(encoding="utf-8")
309 )
310 report = json.loads(report_path.read_text(encoding="utf-8"))
311 self.assertEqual(frontmatter["quality_score"], expected_score)
312 self.assertNotEqual(frontmatter["quality_score"], 99)
313 self.assertEqual(report["quality_breakdown"], expected_breakdown)
314
315 def test_validate_analysis_accepts_block_style_lists(self) -> None:
316 errors, word_count = analysis_gate.validate_analysis(
317 make_analysis(VALID_FRONTMATTER, make_body()),
318 RAW_PAYLOAD,
319 CURRENT_DATETIME,
320 )
321
322 self.assertEqual(errors, [])
323 self.assertGreaterEqual(word_count, 200)
324
325 def test_validate_analysis_rejects_wrong_week_date_and_types(self) -> None:
326 invalid_frontmatter = '''title: "The Week Local Models Went Mainstream"
327 date: 2026-06-01T12:00:00Z
328 week: 2026-W22
329 year: "2026"
330 tags: weekly
331 categories:
332 - analysis
333 repos_featured: 9
334 stars_tracked: 1200
335 top_repo: owner/repo
336 quality_score: 82
337 summary: "A grounded week focused on practical tools."'''.strip()
338
339 errors, _ = analysis_gate.validate_analysis(
340 make_analysis(invalid_frontmatter, make_body()),
341 RAW_PAYLOAD,
342 CURRENT_DATETIME,
343 )
344
345 self.assertIn("week must match raw payload week '2026-W23'.", errors)
346 self.assertIn("year must be an integer.", errors)
347 self.assertIn("tags must be an array of strings.", errors)
348 self.assertIn("categories must include 'weekly'.", errors)
349 self.assertIn("date must match the current run timestamp.", errors)
350
351 def test_validate_analysis_requires_real_heading_lines(self) -> None:
352 body = make_body(
353 alternate_heading="The prose references ## Where Industry Meets Code without creating a heading line.",
354 )
355 errors, _ = analysis_gate.validate_analysis(
356 make_analysis(VALID_FRONTMATTER, body),
357 RAW_PAYLOAD,
358 CURRENT_DATETIME,
359 )
360
361 self.assertIn("Missing required section heading: ## Where Industry Meets Code", errors)
362
363 def test_validate_analysis_allows_legitimate_todo_mentions(self) -> None:
364 errors, _ = analysis_gate.validate_analysis(
365 make_analysis(VALID_FRONTMATTER, make_body(include_todo_app=True)),
366 RAW_PAYLOAD,
367 CURRENT_DATETIME,
368 )
369
370 self.assertEqual(errors, [])
371
372 def test_validate_analysis_rejects_todo_placeholders(self) -> None:
373 body = make_body() + "\n\nTODO: replace this closing note.\n"
374 errors, _ = analysis_gate.validate_analysis(
375 make_analysis(VALID_FRONTMATTER, body),
376 RAW_PAYLOAD,
377 CURRENT_DATETIME,
378 )
379
380 self.assertIn(
381 "Analysis body contains prohibited placeholder marker: TODO placeholder marker", errors
382 )
383
384 def test_validate_analysis_rejects_generic_week_analysis_title(self) -> None:
385 frontmatter = VALID_FRONTMATTER.replace(
386 'title: "The Week Local Models Went Mainstream"',
387 'title: "Week 21, 2026 Analysis"',
388 )
389 errors, _ = analysis_gate.validate_analysis(
390 make_analysis(frontmatter, make_body()),
391 RAW_PAYLOAD,
392 CURRENT_DATETIME,
393 )
394
395 self.assertIn("title must not use a generic week/year placeholder format.", errors)
396
397 def test_validate_analysis_rejects_generic_week_year_title(self) -> None:
398 frontmatter = VALID_FRONTMATTER.replace(
399 'title: "The Week Local Models Went Mainstream"',
400 'title: "Week 21, 2026"',
401 )
402 errors, _ = analysis_gate.validate_analysis(
403 make_analysis(frontmatter, make_body()),
404 RAW_PAYLOAD,
405 CURRENT_DATETIME,
406 )
407
408 self.assertIn("title must not use a generic week/year placeholder format.", errors)
409
410 def test_validate_analysis_accepts_prediction_registry(self) -> None:
411 frontmatter = (
412 VALID_FRONTMATTER
413 + "\npredictions:\n - repo: owner/repo\n claim_type: signal\n direction: up\n confidence: 0.7"
414 )
415
416 errors, _ = analysis_gate.validate_analysis(
417 make_analysis(frontmatter, make_body()),
418 RAW_PAYLOAD,
419 CURRENT_DATETIME,
420 )
421
422 self.assertEqual(errors, [])
423
424 def test_repair_analysis_refuses_to_guess_legacy_prediction_claim_type(self) -> None:
425 frontmatter = (
426 VALID_FRONTMATTER.replace(
427 "date: 2026-06-01T00:00:00Z",
428 "date: 2026-06-01T12:00:00Z",
429 )
430 + "\npredictions:\n - repo: owner/repo\n direction: up\n confidence: 0.7"
431 )
432
433 repaired_text, actions = analysis_gate.repair_analysis(
434 make_analysis(frontmatter, make_body()),
435 RAW_PAYLOAD_WITH_REPOS,
436 CURRENT_DATETIME,
437 )
438 errors, _ = analysis_gate.validate_analysis(
439 repaired_text, RAW_PAYLOAD_WITH_REPOS, CURRENT_DATETIME
440 )
441 frontmatter_after, _ = analysis_gate.extract_frontmatter(repaired_text)
442
443 self.assertEqual(errors, ["predictions[1].claim_type must be one of signal, noise, gap."])
444 self.assertIn("set date from current run timestamp", actions)
445 self.assertNotIn("claim_type", frontmatter_after["predictions"][0])
446 self.assertEqual(frontmatter_after["repos_featured"], 3)
447 self.assertEqual(frontmatter_after["stars_tracked"], 1500)
448
449 def test_repair_analysis_normalizes_safe_prediction_claim_alias(self) -> None:
450 frontmatter = (
451 VALID_FRONTMATTER
452 + "\npredictions:\n - repo: owner/repo\n claim: Signal\n direction: UP\n confidence: 0.7"
453 )
454
455 repaired_text, actions = analysis_gate.repair_analysis(
456 make_analysis(frontmatter, make_body()),
457 RAW_PAYLOAD,
458 CURRENT_DATETIME,
459 )
460 errors, _ = analysis_gate.validate_analysis(repaired_text, RAW_PAYLOAD, CURRENT_DATETIME)
461 frontmatter_after, _ = analysis_gate.extract_frontmatter(repaired_text)
462
463 self.assertEqual(errors, [])
464 self.assertIn("set predictions[1].claim_type from claim", actions)
465 self.assertEqual(frontmatter_after["predictions"][0]["claim_type"], "signal")
466 self.assertNotIn("claim", frontmatter_after["predictions"][0])
467
468 def test_validate_analysis_rejects_invalid_prediction_registry(self) -> None:
469 frontmatter = (
470 VALID_FRONTMATTER
471 + "\npredictions:\n - repo: bad repo\n claim_type: maybe\n direction: sideways\n confidence: 1.3\n note: nope"
472 )
473
474 errors, _ = analysis_gate.validate_analysis(
475 make_analysis(frontmatter, make_body()),
476 RAW_PAYLOAD,
477 CURRENT_DATETIME,
478 )
479
480 self.assertIn("predictions[1].repo must use owner/repo format.", errors)
481 self.assertIn("predictions[1].claim_type must be one of signal, noise, gap.", errors)
482 self.assertIn("predictions[1].direction must be one of up, flat, down.", errors)
483 self.assertIn("predictions[1].confidence must be between 0 and 1.", errors)
484 self.assertIn("predictions[1] has unexpected fields: note", errors)
485
486 def test_prediction_contract_examples_stay_aligned_with_gate(self) -> None:
487 repo_root = Path(__file__).resolve().parent.parent
488 docs = (repo_root / "docs" / "analysis-spec.md").read_text(encoding="utf-8")
489 prompt = (repo_root / "prompts" / "analyze-weekly.md").read_text(encoding="utf-8")
490
491 for content in (docs, prompt):
492 self.assertIn("{repo, claim_type, direction, confidence}", content)
493 self.assertIn("signal|noise|gap", content)
494 self.assertNotIn("{repo, direction, confidence}", content)
495
496 def test_gate_report_fingerprint_tolerates_missing_or_invalid_reports(self) -> None:
497 tests_root = Path(__file__).resolve().parent
498 with tempfile.TemporaryDirectory(dir=tests_root) as tmpdir:
499 workspace = Path(tmpdir)
500 missing = workspace / "missing.json"
501 invalid = workspace / "invalid.json"
502 report = workspace / "report.json"
503 invalid.write_text("not json", encoding="utf-8")
504 report.write_text(
505 json.dumps({"errors_after_repair": ["date must match the current run timestamp."]}),
506 encoding="utf-8",
507 )
508
509 self.assertEqual(analysis_gate.gate_report_fingerprint(missing), "")
510 self.assertEqual(analysis_gate.gate_report_fingerprint(invalid), "")
511 self.assertRegex(analysis_gate.gate_report_fingerprint(report), r"^[0-9a-f]{64}$")
512
513 def test_fallback_frontmatter_dump_handles_empty_dict_list_items(self) -> None:
514 original_yaml = analysis_gate.yaml
515 try:
516 analysis_gate.yaml = None
517 dumped = analysis_gate.dump_frontmatter({"predictions": [{}]})
518 finally:
519 analysis_gate.yaml = original_yaml
520
521 self.assertIn(" - {}", dumped)
522
523 def test_repair_exception_still_writes_gate_report(self) -> None:
524 tests_root = Path(__file__).resolve().parent
525 with tempfile.TemporaryDirectory(dir=tests_root) as tmpdir:
526 workspace = Path(tmpdir)
527 analysis_path = workspace / "candidate.md"
528 raw_path = workspace / "raw.json"
529 report_path = workspace / "report.json"
530 analysis_path.write_text(
531 make_analysis(VALID_FRONTMATTER + "\npredictions:\n - {}", make_body()),
532 encoding="utf-8",
533 )
534 raw_path.write_text('{"week": "2026-W23"}', encoding="utf-8")
535
536 with mock.patch.object(
537 analysis_gate, "repair_analysis", side_effect=RuntimeError("boom")
538 ):
539 with self.assertRaises(SystemExit) as raised:
540 analysis_gate.main(
541 [
542 "--analysis-file",
543 str(analysis_path),
544 "--raw-json",
545 str(raw_path),
546 "--current-datetime",
547 CURRENT_DATETIME,
548 "--repair-safe",
549 "--report-json",
550 str(report_path),
551 ]
552 )
553
554 self.assertEqual(raised.exception.code, 1)
555 report = analysis_gate.load_json(report_path)
556 self.assertEqual(report["repair_actions"], ["repair skipped: boom"])
557 self.assertIn(
558 "predictions[1].repo must use owner/repo format.", report["errors_after_repair"]
559 )
560
561 def test_gate_report_captures_pre_repair_publish_errors_from_original_text(self) -> None:
562 tests_root = Path(__file__).resolve().parent
563 with tempfile.TemporaryDirectory(dir=tests_root) as tmpdir:
564 workspace = Path(tmpdir)
565 analysis_path = workspace / "candidate.md"
566 raw_path = workspace / "raw.json"
567 report_path = workspace / "report.json"
568 original_text = make_analysis(
569 VALID_FRONTMATTER.replace(
570 "date: 2026-06-01T00:00:00Z", "date: 2026-06-01T12:00:00Z"
571 ),
572 make_body(),
573 )
574 analysis_path.write_text(original_text, encoding="utf-8")
575 raw_path.write_text(json.dumps(RAW_PAYLOAD_WITH_REPOS), encoding="utf-8")
576
577 def publish_quality_for(
578 text: str,
579 raw_payload: dict,
580 *,
581 source: str,
582 model: str,
583 press_context_available: bool = False,
584 ) -> tuple[list[str], dict]:
585 if text == original_text:
586 return ["pre-repair publish-quality failure"], analysis_gate.build_gate_results(
587 ["pre-repair publish-quality failure"]
588 )
589 return [], analysis_gate.build_gate_results([])
590
591 with mock.patch.object(
592 analysis_gate, "validate_publish_quality", side_effect=publish_quality_for
593 ):
594 self.assertEqual(
595 analysis_gate.main(
596 [
597 "--analysis-file",
598 str(analysis_path),
599 "--raw-json",
600 str(raw_path),
601 "--current-datetime",
602 CURRENT_DATETIME,
603 "--repair-safe",
604 "--report-json",
605 str(report_path),
606 ]
607 ),
608 0,
609 )
610
611 report = analysis_gate.load_json(report_path)
612 self.assertIn("pre-repair publish-quality failure", report["errors_before_repair"])
613 self.assertNotIn("pre-repair publish-quality failure", report["errors_after_repair"])
614
615 def test_publish_quality_gate_rejects_structurally_valid_low_quality_summary(self) -> None:
616 generic = " ".join(
617 ["Projects were active this week and many updates appeared across the list."] * 12
618 )
619 low_quality = f"""
620 ## This Week's Trends
621
622 {generic}
623
624 ## Where Industry Meets Code
625
626 {generic}
627
628 ## Signal & Noise
629
630 {generic}
631
632 ## Blind Spots
633
634 {generic}
635
636 ## The Week Ahead
637
638 {generic}
639
640 ## Key References
641
642 ### Notable Projects
643
644 - [owner/repo-a](https://github.com/owner/repo-a) — appeared in the list.
645 - [owner/repo-b](https://github.com/owner/repo-b) — appeared in the list.
646
647 ### Press & Industry
648
649 No press data was provided this week.
650 """.strip()
651 errors, gates = analysis_gate.validate_publish_quality(
652 make_analysis(VALID_FRONTMATTER, low_quality),
653 RAW_PAYLOAD,
654 source="copilot-cli",
655 model="copilot-default",
656 )
657
658 self.assertTrue(any("editorial analysis" in error for error in errors))
659 self.assertFalse(gates["editorial_quality"]["passed"])
660
661 def test_publish_quality_gate_accepts_known_good_weekly_outputs(self) -> None:
662 repo_root = Path(__file__).resolve().parent.parent
663 fixtures = [
664 (
665 "2026-W22",
666 "2026-05-25T11:56:08Z",
667 "perplexityai/bumblebee",
668 repo_root / "data/analyzed/2026-W22-summary.md",
669 ),
670 (
671 "2026-W23",
672 "2026-06-06T07:49:43Z",
673 "pewdiepie-archdaemon/odysseus",
674 repo_root / "data/analyzed/2026-W23-summary.md",
675 ),
676 ]
677
678 for week, crawled_at, repo_name, summary_path in fixtures:
679 with self.subTest(week=week):
680 raw_payload = {
681 "week": week,
682 "crawled_at": crawled_at,
683 "new_repos": [{"full_name": repo_name, "stars": 100}],
684 "trending_repos": [],
685 }
686 text = summary_path.read_text(encoding="utf-8")
687 linked_repos = sorted(analysis_gate.REPO_LINK_PATTERN.findall(text))
688 raw_payload["new_repos"].extend(
689 {"full_name": name, "stars": 100} for name in linked_repos if name != repo_name
690 )
691
692 structure_errors, word_count = analysis_gate.validate_analysis(
693 text, raw_payload, crawled_at
694 )
695 publish_errors, gates = analysis_gate.validate_publish_quality(
696 text,
697 raw_payload,
698 source="copilot-cli",
699 model="copilot-default",
700 )
701
702 self.assertEqual(structure_errors, [])
703 self.assertGreater(word_count, 200)
704 self.assertEqual(publish_errors, [])
705 self.assertTrue(all(gate["passed"] for gate in gates.values()))
706
707 def test_copilot_source_without_explicit_model_uses_publishable_default(self) -> None:
708 errors, gates = analysis_gate.validate_publish_quality(
709 make_analysis(VALID_FRONTMATTER, make_body()),
710 RAW_PAYLOAD_WITH_REPOS,
711 source="copilot-cli",
712 model=analysis_gate.parse_args(
713 [
714 "--analysis-file",
715 "candidate.md",
716 "--raw-json",
717 "raw.json",
718 "--current-datetime",
719 CURRENT_DATETIME,
720 "--source",
721 "copilot-cli",
722 ]
723 ).model,
724 )
725
726 self.assertEqual(errors, [])
727 self.assertTrue(gates["ai_provenance"]["passed"])
728
729 def test_publish_quality_gate_rejects_missing_evidence_citations(self) -> None:
730 body = (
731 make_body()
732 .replace("[owner/repo-a](https://github.com/owner/repo-a)", "owner/repo-a")
733 .replace(
734 "[owner/repo-b](https://github.com/owner/repo-b)",
735 "owner/repo-b",
736 )
737 )
738 errors, gates = analysis_gate.validate_publish_quality(
739 make_analysis(VALID_FRONTMATTER, body),
740 RAW_PAYLOAD_WITH_REPOS,
741 source="copilot-cli",
742 model="copilot-default",
743 )
744
745 self.assertIn(
746 "evidence citations must include at least one repository link from the raw payload.",
747 errors,
748 )
749 self.assertFalse(gates["evidence_citation"]["passed"])
750
751 def test_publish_quality_gate_rejects_repo_links_outside_current_inventory(self) -> None:
752 body = make_body().replace(
753 "[owner/repo-a](https://github.com/owner/repo-a)",
754 "[other/repo](https://github.com/other/repo)",
755 )
756 errors, gates = analysis_gate.validate_publish_quality(
757 make_analysis(VALID_FRONTMATTER, body),
758 RAW_PAYLOAD_WITH_REPOS,
759 source="copilot-cli",
760 model="copilot-default",
761 )
762
763 self.assertIn(
764 "repository links must resolve to the current raw evidence inventory: other/repo.",
765 errors,
766 )
767 self.assertFalse(gates["evidence_citation"]["passed"])
768
769 def test_publish_quality_gate_rejects_stale_evidence(self) -> None:
770 stale_payload = dict(RAW_PAYLOAD_WITH_REPOS, crawled_at="2026-05-25T00:00:00Z")
771 errors, gates = analysis_gate.validate_publish_quality(
772 make_analysis(VALID_FRONTMATTER, make_body()),
773 stale_payload,
774 source="copilot-cli",
775 model="copilot-default",
776 )
777
778 self.assertTrue(any("raw evidence timestamp week mismatch" in error for error in errors))
779 self.assertFalse(gates["evidence_citation"]["passed"])
780
781 def test_publish_quality_gate_prefers_generated_at_for_republished_evidence(self) -> None:
782 republished_payload = dict(
783 RAW_PAYLOAD_WITH_REPOS,
784 crawled_at="2026-05-25T00:00:00Z",
785 generated_at="2026-06-01T00:00:00Z",
786 )
787 errors, gates = analysis_gate.validate_publish_quality(
788 make_analysis(VALID_FRONTMATTER, make_body()),
789 republished_payload,
790 source="copilot-cli",
791 model="copilot-default",
792 )
793
794 self.assertEqual(errors, [])
795 self.assertTrue(gates["evidence_citation"]["passed"])
796
797 def test_publish_quality_gate_rejects_no_ai_provenance(self) -> None:
798 errors, gates = analysis_gate.validate_publish_quality(
799 make_analysis(VALID_FRONTMATTER, make_body()),
800 RAW_PAYLOAD,
801 source="no-ai",
802 model="none",
803 )
804
805 self.assertIn("AI provenance source is not publishable: no-ai.", errors)
806 self.assertIn("AI provenance model is not publishable: none.", errors)
807 self.assertFalse(gates["ai_provenance"]["passed"])
808
809 def test_publish_quality_gate_rejects_github_models_provenance(self) -> None:
810 errors, gates = analysis_gate.validate_publish_quality(
811 make_analysis(VALID_FRONTMATTER, make_body()),
812 RAW_PAYLOAD,
813 source="github-models",
814 model="openai/gpt-4o",
815 )
816
817 self.assertIn("AI provenance source is not publishable: github-models.", errors)
818 self.assertFalse(gates["ai_provenance"]["passed"])
819
820 def test_gate_report_includes_structured_failure_summary(self) -> None:
821 errors = [
822 "repository links must resolve to the current raw evidence inventory: other/repo."
823 ]
824 gates = analysis_gate.build_gate_results(errors)
825 summary = analysis_gate.build_failure_summary(errors, gates)
826
827 self.assertEqual(summary["failure_class"], "evidence_citation")
828 self.assertEqual(summary["failure_categories"], ["evidence_citation"])
829 self.assertEqual(summary["error_count"], 1)
830
831 def test_publish_quality_gate_rejects_contradictory_press_claims(self) -> None:
832 body = (
833 make_body()
834 + "\n\nNo press data was provided this week, but TechCrunch reported a major launch."
835 )
836 errors, gates = analysis_gate.validate_publish_quality(
837 make_analysis(VALID_FRONTMATTER, body),
838 RAW_PAYLOAD,
839 source="copilot-cli",
840 model="copilot-default",
841 )
842
843 self.assertTrue(any("contradictory claim" in error for error in errors))
844 self.assertFalse(gates["editorial_quality"]["passed"])
845
846 def test_stale_press_claim_fails_when_press_context_available(self) -> None:
847 """Regression (2026-W30): body claims no press data while a populated press
848 context exists → gate must fail with a 'stale press claim:' error."""
849 body = "No industry press data was available for this week's analysis."
850 errors = analysis_gate.stale_press_claim_errors(body, press_context_available=True)
851 self.assertTrue(any(error.startswith("stale press claim:") for error in errors))
852
853 def test_stale_press_claim_ignored_when_no_press_context(self) -> None:
854 """Legitimately press-less weeks must NOT false-positive: the same body with
855 press_context_available=False produces no stale-press error."""
856 body = "No industry press data was available for this week's analysis."
857 errors = analysis_gate.stale_press_claim_errors(body, press_context_available=False)
858 self.assertEqual(errors, [])
859
860 def test_stale_press_claim_catches_key_references_variant(self) -> None:
861 """The 'No press data was provided this week.' Key References phrasing is also
862 caught when a populated press context exists."""
863 body = "### Press & Industry\n\nNo press data was provided this week."
864 errors = analysis_gate.stale_press_claim_errors(body, press_context_available=True)
865 self.assertTrue(any(error.startswith("stale press claim:") for error in errors))
866 errors_no_press = analysis_gate.stale_press_claim_errors(
867 body, press_context_available=False
868 )
869 self.assertEqual(errors_no_press, [])
870
871 def test_publish_quality_gate_fails_on_stale_press_claim_with_press_available(self) -> None:
872 """End-to-end: validate_publish_quality wires stale_press_claim_errors into the
873 editorial_quality gate when press context is available."""
874 body = make_body() + "\n\nNo industry press data was available for this week's analysis."
875 errors, gates = analysis_gate.validate_publish_quality(
876 make_analysis(VALID_FRONTMATTER, body),
877 RAW_PAYLOAD,
878 source="copilot-cli",
879 model="copilot-default",
880 press_context_available=True,
881 )
882 self.assertTrue(any(error.startswith("stale press claim:") for error in errors))
883 self.assertFalse(gates["editorial_quality"]["passed"])
884
885 def test_publish_quality_gate_allows_no_press_body_when_press_absent(self) -> None:
886 """The same body passes the stale-press rule when there is genuinely no press
887 context (the default press_context_available=False)."""
888 body = make_body() + "\n\nNo industry press data was available for this week's analysis."
889 errors, gates = analysis_gate.validate_publish_quality(
890 make_analysis(VALID_FRONTMATTER, body),
891 RAW_PAYLOAD,
892 source="copilot-cli",
893 model="copilot-default",
894 press_context_available=False,
895 )
896 self.assertFalse(any(error.startswith("stale press claim:") for error in errors))
897
898 def test_press_context_is_populated_detects_real_and_empty_context(self) -> None:
899 """The helper treats missing/empty files and the render sentinel as empty, but
900 real content (or a positive token estimate) as populated."""
901 self.assertFalse(analysis_gate.press_context_is_populated(None))
902 tests_root = Path(__file__).resolve().parent
903 with tempfile.TemporaryDirectory(dir=tests_root) as tmpdir:
904 base = Path(tmpdir)
905
906 missing = base / "missing-press-context.md"
907 self.assertFalse(analysis_gate.press_context_is_populated(missing))
908
909 empty = base / "empty-press-context.md"
910 empty.write_text("", encoding="utf-8")
911 self.assertFalse(analysis_gate.press_context_is_populated(empty))
912
913 sentinel = base / "sentinel-press-context.md"
914 sentinel.write_text("No press data available for this week.", encoding="utf-8")
915 self.assertFalse(analysis_gate.press_context_is_populated(sentinel))
916
917 real = base / "real-press-context.md"
918 real.write_text(
919 "## Press Context\n\n22 relevant articles about AI agents.",
920 encoding="utf-8",
921 )
922 self.assertTrue(analysis_gate.press_context_is_populated(real))
923
924 # A positive token estimate short-circuits to populated even without a file.
925 self.assertTrue(analysis_gate.press_context_is_populated(missing, token_estimate=42))
926
927 def test_press_context_is_populated_sentinel_wins_over_token_estimate(self) -> None:
928 """Regression (2026-W30 press-less path): a provided path is authoritative, so a
929 sentinel-only press file is *not* populated even when token_estimate > 0, while a
930 real press file is. The token_estimate fallback only applies when no usable path
931 is provided."""
932 tests_root = Path(__file__).resolve().parent
933 with tempfile.TemporaryDirectory(dir=tests_root) as tmpdir:
934 base = Path(tmpdir)
935
936 sentinel = base / "sentinel-press-context.md"
937 sentinel.write_text(NO_PRESS_SENTINEL, encoding="utf-8")
938 self.assertFalse(analysis_gate.press_context_is_populated(sentinel, token_estimate=500))
939
940 real = base / "real-press-context.md"
941 real.write_text(
942 "## Press Context\n\n22 relevant articles about AI agents.",
943 encoding="utf-8",
944 )
945 self.assertTrue(analysis_gate.press_context_is_populated(real, token_estimate=500))
946
947 # token_estimate fallback applies only when no usable path is provided.
948 self.assertTrue(analysis_gate.press_context_is_populated(None, token_estimate=500))
949 self.assertFalse(analysis_gate.press_context_is_populated(None, 0))
950 self.assertFalse(analysis_gate.press_context_is_populated(None, None))
951
952 def test_press_context_token_estimate_is_fallback_only(self) -> None:
953 """Positive token estimates populate only when no readable authoritative content exists."""
954 self.assertTrue(analysis_gate.press_context_is_populated(None, token_estimate=1))
955 self.assertFalse(analysis_gate.press_context_is_populated(None, token_estimate=0))
956 self.assertFalse(analysis_gate.press_context_is_populated(None, token_estimate=None))
957
958 tests_root = Path(__file__).resolve().parent
959 with tempfile.TemporaryDirectory(dir=tests_root) as tmpdir:
960 base = Path(tmpdir)
961
962 missing = base / "missing-press-context.md"
963 self.assertTrue(analysis_gate.press_context_is_populated(missing, token_estimate=1))
964 self.assertFalse(analysis_gate.press_context_is_populated(missing, token_estimate=0))
965 self.assertFalse(analysis_gate.press_context_is_populated(missing, token_estimate=None))
966
967 empty = base / "empty-press-context.md"
968 empty.write_text("", encoding="utf-8")
969 self.assertTrue(analysis_gate.press_context_is_populated(empty, token_estimate=1))
970 self.assertFalse(analysis_gate.press_context_is_populated(empty, token_estimate=0))
971
972 unreadable = base / "unreadable-press-context.md"
973 unreadable.write_text("content that cannot be read", encoding="utf-8")
974 with mock.patch.object(Path, "read_text", side_effect=OSError("unreadable")):
975 self.assertTrue(
976 analysis_gate.press_context_is_populated(unreadable, token_estimate=1)
977 )
978
979 sentinel = base / "sentinel-press-context.md"
980 sentinel.write_text(NO_PRESS_SENTINEL, encoding="utf-8")
981 self.assertFalse(
982 analysis_gate.press_context_is_populated(sentinel, token_estimate=9999)
983 )
984
985 real = base / "real-press-context.md"
986 real.write_text(
987 "## Press Context\n\n22 relevant articles about AI agents.",
988 encoding="utf-8",
989 )
990 self.assertTrue(analysis_gate.press_context_is_populated(real, token_estimate=0))
991
992 def test_press_context_fallback_uses_rendered_content_token_estimate(self) -> None:
993 """The gate fallback consumes render_press_context.press_token_estimate, where
994 empty/whitespace/sentinel content maps to 0 and real press content maps positive."""
995 self.assertFalse(analysis_gate.press_context_is_populated(None, press_token_estimate("")))
996 self.assertFalse(
997 analysis_gate.press_context_is_populated(None, press_token_estimate(" \n\t "))
998 )
999 self.assertFalse(
1000 analysis_gate.press_context_is_populated(None, press_token_estimate(NO_PRESS_SENTINEL))
1001 )
1002 self.assertTrue(
1003 analysis_gate.press_context_is_populated(
1004 None,
1005 press_token_estimate("## Press Context\n\nA real article about AI agents."),
1006 )
1007 )
1008
1009 def test_stale_press_gate_end_to_end_for_sentinel_pressless_week(self) -> None:
1010 """End-to-end press-less path: a body that legitimately states press was absent
1011 must NOT trip the stale-press rule when the week's press file is the sentinel
1012 (even with a positive token estimate); the identical body with a real press file
1013 still trips it, confirming the W30 regression stays caught."""
1014 body = "No industry press data was available for this week's analysis."
1015 tests_root = Path(__file__).resolve().parent
1016 with tempfile.TemporaryDirectory(dir=tests_root) as tmpdir:
1017 base = Path(tmpdir)
1018 token_estimate = 500
1019
1020 sentinel = base / "sentinel-press-context.md"
1021 sentinel.write_text(NO_PRESS_SENTINEL, encoding="utf-8")
1022 sentinel_available = analysis_gate.press_context_is_populated(
1023 sentinel, token_estimate=token_estimate
1024 )
1025 self.assertFalse(sentinel_available)
1026 self.assertEqual(
1027 analysis_gate.stale_press_claim_errors(
1028 body, press_context_available=sentinel_available
1029 ),
1030 [],
1031 )
1032
1033 real = base / "real-press-context.md"
1034 real.write_text(
1035 "## Press Context\n\n22 relevant articles about AI agents.",
1036 encoding="utf-8",
1037 )
1038 real_available = analysis_gate.press_context_is_populated(
1039 real, token_estimate=token_estimate
1040 )
1041 self.assertTrue(real_available)
1042 errors = analysis_gate.stale_press_claim_errors(
1043 body, press_context_available=real_available
1044 )
1045 self.assertTrue(any(error.startswith("stale press claim:") for error in errors))
1046
1047
1048 if __name__ == "__main__":
1049 unittest.main()