15
from urllib import error, parse, request
16
17
try:
18
- from scripts.assemble_historical_context import DEFAULT_CONTENT_ROOT, assemble_historical_context
18
+ from scripts.assemble_historical_context import (
19
+ DEFAULT_CONTENT_ROOT,
20
+ assemble_historical_context,
21
+ )
22
from scripts.learned_context import render_continuity
23
from scripts.sanitize_repo_content import sanitize_repo_payload
24
except ModuleNotFoundError: # pragma: no cover - script execution path
25
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
23
- from scripts.assemble_historical_context import DEFAULT_CONTENT_ROOT, assemble_historical_context
26
+ from scripts.assemble_historical_context import (
27
+ DEFAULT_CONTENT_ROOT,
28
+ assemble_historical_context,
29
+ )
30
from scripts.learned_context import render_continuity
31
from scripts.sanitize_repo_content import sanitize_repo_payload
32
38
DEFAULT_CONTINUITY_FILE = ROOT / ".squad" / "identity" / "continuity.md"
39
DEFAULT_MODELS_ENDPOINT = "https://models.github.ai/inference/chat/completions"
40
DEFAULT_MODELS_MODEL = "openai/gpt-4o"
41
+# Synthesis step defaults to the same GitHub Models model unless overridden.
42
+DEFAULT_SYNTHESIS_MODEL = DEFAULT_MODELS_MODEL
43
DEFAULT_MODELS_TIMEOUT = 30
44
ALLOWED_MODELS_HOSTS: frozenset[str] = frozenset({"models.github.ai"})
45
_JITTER_RANDOM = secrets.SystemRandom()
154
155
156
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
149
- parser = argparse.ArgumentParser(description="Render/preflight weekly analysis prompts or generate diagnostic no-AI output.")
150
- parser.add_argument("--raw-json", required=True, type=Path, help="Path to the weekly raw JSON payload.")
151
- parser.add_argument("--output", required=True, type=Path, help="Path to write the analyzed markdown output.")
152
- parser.add_argument("--current-datetime", required=True, help="ISO-8601 timestamp for the analysis run.")
157
+ parser = argparse.ArgumentParser(
158
+ description="Render/preflight weekly analysis prompts or generate diagnostic no-AI output."
159
+ )
160
+ parser.add_argument(
161
+ "--raw-json", required=True, type=Path, help="Path to the weekly raw JSON payload."
162
+ )
163
+ parser.add_argument(
164
+ "--output", required=True, type=Path, help="Path to write the analyzed markdown output."
165
+ )
166
+ parser.add_argument(
167
+ "--current-datetime", required=True, help="ISO-8601 timestamp for the analysis run."
168
+ )
169
parser.add_argument(
170
"--prompt-template",
171
type=Path,
303
def _repo_topics(value: Any) -> list[str]:
304
if not isinstance(value, list):
305
return []
290
- return [str(topic) for topic in value if isinstance(topic, (str, int, float)) and str(topic).strip()]
306
+ return [
307
+ str(topic) for topic in value if isinstance(topic, (str, int, float)) and str(topic).strip()
308
+ ]
309
310
311
REQUIRED_REPO_SLICE_FIELDS = (
325
url = repo.get("url")
326
return {
327
"full_name": full_name,
310
- "url": url if isinstance(url, str) and url.strip() else (f"https://github.com/{full_name}" if full_name else None),
311
- "description": repo.get("description") if isinstance(repo.get("description"), str) else None,
328
+ "url": url
329
+ if isinstance(url, str) and url.strip()
330
+ else (f"https://github.com/{full_name}" if full_name else None),
331
+ "description": repo.get("description")
332
+ if isinstance(repo.get("description"), str)
333
+ else None,
334
"language": repo.get("language") if isinstance(repo.get("language"), str) else None,
335
"topics": _repo_topics(repo.get("topics")),
336
"stars": _repo_int(repo.get("stars")),
356
EvidenceRepoRef(
357
full_name=full_name.strip(),
358
url=url if isinstance(url, str) and url.strip() else None,
337
- description=repo.get("description") if isinstance(repo.get("description"), str) else None,
359
+ description=repo.get("description")
360
+ if isinstance(repo.get("description"), str)
361
+ else None,
362
language=repo.get("language") if isinstance(repo.get("language"), str) else None,
363
topics=_repo_topics(repo.get("topics")),
364
source=field,
365
stars=_repo_int(repo.get("stars")),
366
stars_gained=_repo_int(repo.get("stars_gained")),
343
- created_at=repo.get("created_at") if isinstance(repo.get("created_at"), str) else None,
367
+ created_at=repo.get("created_at")
368
+ if isinstance(repo.get("created_at"), str)
369
+ else None,
370
)
371
)
372
return refs
373
374
349
-def _evidence_inventory(name: str, payload: dict[str, Any], field: str, path: Path) -> EvidenceInventory:
375
+def _evidence_inventory(
376
+ name: str, payload: dict[str, Any], field: str, path: Path
377
+) -> EvidenceInventory:
378
content = json.dumps(payload.get(field, []), indent=2, ensure_ascii=False)
379
repos = _inventory_repo_refs(payload, field)
380
return EvidenceInventory(
388
)
389
390
363
-def _press_paths_for_context(press_context_path: Path | None, week: str) -> tuple[Path | None, Path | None]:
391
+def _press_paths_for_context(
392
+ press_context_path: Path | None, week: str
393
+) -> tuple[Path | None, Path | None]:
394
if press_context_path is None:
395
return None, None
396
data_dir = press_context_path.parent.parent
397
external_path = data_dir / "raw" / f"{week}-external-news.json"
398
legacy_path = data_dir / "raw" / f"{week}-techcrunch.json"
399
corr_path = data_dir / "analyzed" / f"{week}-correlations.json"
370
- news_path = external_path if external_path.exists() else legacy_path if legacy_path.exists() else None
400
+ news_path = (
401
+ external_path if external_path.exists() else legacy_path if legacy_path.exists() else None
402
+ )
403
return news_path, corr_path if corr_path.exists() else None
404
405
413
return payload if isinstance(payload, dict) else None
414
415
384
-def _article_inventory(news_payload: dict[str, Any] | None, correlation_payload: dict[str, Any] | None, path: Path | None) -> PressInventory:
416
+def _article_inventory(
417
+ news_payload: dict[str, Any] | None,
418
+ correlation_payload: dict[str, Any] | None,
419
+ path: Path | None,
420
+) -> PressInventory:
421
articles = news_payload.get("articles", []) if news_payload else []
422
correlations = correlation_payload.get("correlations", []) if correlation_payload else []
423
repo_by_url: dict[str, set[str]] = {}
425
if not isinstance(corr, dict):
426
continue
427
repo = corr.get("repo")
392
- for url in corr.get("matched_articles", []) if isinstance(corr.get("matched_articles"), list) else []:
428
+ for url in (
429
+ corr.get("matched_articles", [])
430
+ if isinstance(corr.get("matched_articles"), list)
431
+ else []
432
+ ):
433
if isinstance(url, str) and isinstance(repo, str):
434
repo_by_url.setdefault(url, set()).add(repo)
395
- for detail in corr.get("matched_article_details", []) if isinstance(corr.get("matched_article_details"), list) else []:
396
- if isinstance(detail, dict) and isinstance(detail.get("url"), str) and isinstance(repo, str):
435
+ for detail in (
436
+ corr.get("matched_article_details", [])
437
+ if isinstance(corr.get("matched_article_details"), list)
438
+ else []
439
+ ):
440
+ if (
441
+ isinstance(detail, dict)
442
+ and isinstance(detail.get("url"), str)
443
+ and isinstance(repo, str)
444
+ ):
445
repo_by_url.setdefault(detail["url"], set()).add(repo)
446
refs: list[EvidencePressRef] = []
447
for article in articles if isinstance(articles, list) else []:
400
- if not isinstance(article, dict) or not isinstance(article.get("url"), str) or not article["url"].strip():
448
+ if (
449
+ not isinstance(article, dict)
450
+ or not isinstance(article.get("url"), str)
451
+ or not article["url"].strip()
452
+ ):
453
continue
402
- categories = article.get("categories") if isinstance(article.get("categories"), list) else []
454
+ categories = (
455
+ article.get("categories") if isinstance(article.get("categories"), list) else []
456
+ )
457
relevance = article.get("relevance_score")
458
refs.append(
459
EvidencePressRef(
460
title=article.get("title") if isinstance(article.get("title"), str) else None,
461
url=article["url"],
462
source=article.get("source") if isinstance(article.get("source"), str) else None,
409
- published_at=article.get("published_at") if isinstance(article.get("published_at"), str) else None,
463
+ published_at=article.get("published_at")
464
+ if isinstance(article.get("published_at"), str)
465
+ else None,
466
categories=[str(category) for category in categories],
411
- relevance_score=float(relevance) if isinstance(relevance, (int, float)) and not isinstance(relevance, bool) else None,
467
+ relevance_score=float(relevance)
468
+ if isinstance(relevance, (int, float)) and not isinstance(relevance, bool)
469
+ else None,
470
correlation_repos=sorted(repo_by_url.get(article["url"], set())),
471
)
472
)
489
if path is None or not path.exists():
490
return None
491
data = path.read_bytes()
434
- return {"path": path.as_posix(), "bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()}
492
+ return {
493
+ "path": path.as_posix(),
494
+ "bytes": len(data),
495
+ "sha256": hashlib.sha256(data).hexdigest(),
496
+ }
497
encoded = content.encode("utf-8")
498
return {
499
"path": path.as_posix() if path else None,
508
return stripped
509
510
449
-def validate_evidence_slice(payload: dict[str, Any], *, expected_checksum: str | None = None) -> list[str]:
511
+def validate_evidence_slice(
512
+ payload: dict[str, Any], *, expected_checksum: str | None = None
513
+) -> list[str]:
514
errors: list[str] = []
451
- for field in ("schema_version", "slice_name", "component", "records", "provenance", "checksum_sha256"):
515
+ for field in (
516
+ "schema_version",
517
+ "slice_name",
518
+ "component",
519
+ "records",
520
+ "provenance",
521
+ "checksum_sha256",
522
+ ):
523
if field not in payload:
524
errors.append(f"slice missing {field}")
525
checksum = payload.get("checksum_sha256")
544
errors.append("slice provenance sources missing")
545
else:
546
for name, source in sources.items():
476
- if not isinstance(source, dict) or not source.get("sha256") or not isinstance(source.get("bytes"), int):
547
+ if (
548
+ not isinstance(source, dict)
549
+ or not source.get("sha256")
550
+ or not isinstance(source.get("bytes"), int)
551
+ ):
552
errors.append(f"slice provenance source {name} missing checksum/bytes")
553
if payload.get("component") in {"new_repos", "trending_repos"}:
554
for index, record in enumerate(records):
561
return errors
562
563
489
-def _build_slice(name: str, records: list[dict[str, Any]], provenance: dict[str, Any]) -> dict[str, Any]:
564
+def _build_slice(
565
+ name: str, records: list[dict[str, Any]], provenance: dict[str, Any]
566
+) -> dict[str, Any]:
567
payload = {
568
"schema_version": "analysis_evidence_slice_v1",
569
"slice_name": name,
588
) -> dict[str, dict[str, Any]]:
589
raw_source = _source_ref(raw_path)
590
press_source = _source_ref(press_context_path, press_content) if press_content else None
514
- previous_source = _source_ref(previous_summary_path, previous_summary_content) if previous_summary_content else None
591
+ previous_source = (
592
+ _source_ref(previous_summary_path, previous_summary_content)
593
+ if previous_summary_content
594
+ else None
595
+ )
596
news_path, corr_path = _press_paths_for_context(press_context_path, week)
516
- news_payload = _safe_load_json(news_path)
597
corr_payload = _safe_load_json(corr_path)
598
news_source = _source_ref(news_path)
599
corr_source = _source_ref(corr_path)
601
slices = {
602
"new_repos": _build_slice(
603
"new_repos",
524
- [compact_repo_record(repo, source="new_repos") for repo in payload_for_prompt.get("new_repos", []) if isinstance(repo, dict)],
604
+ [
605
+ compact_repo_record(repo, source="new_repos")
606
+ for repo in payload_for_prompt.get("new_repos", [])
607
+ if isinstance(repo, dict)
608
+ ],
609
base_provenance,
610
),
611
"trending_repos": _build_slice(
619
),
620
}
621
press_sources = {}
538
- for key, source in (("raw_json", raw_source), ("press_context", press_source), ("external_news", news_source), ("correlations", corr_source)):
622
+ for key, source in (
623
+ ("raw_json", raw_source),
624
+ ("press_context", press_source),
625
+ ("external_news", news_source),
626
+ ("correlations", corr_source),
627
+ ):
628
if source:
629
press_sources[key] = source
630
press_records: list[dict[str, Any]] = []
644
)
645
if not press_records and press_content:
646
urls = sorted(set(re.findall(r"https?://[^\s)\]]+", press_content)))
558
- press_records = [{"url": url.rstrip(".,"), "source": "rendered_press_context"} for url in urls]
647
+ press_records = [
648
+ {"url": url.rstrip(".,"), "source": "rendered_press_context"} for url in urls
649
+ ]
650
slices["press_correlations"] = _build_slice(
651
"press_correlations",
652
press_records,
669
return slices
670
671
581
-def write_evidence_slices(slices: dict[str, dict[str, Any]], manifest_path: Path | None) -> list[EvidenceSliceRef]:
672
+def write_evidence_slices(
673
+ slices: dict[str, dict[str, Any]], manifest_path: Path | None
674
+) -> list[EvidenceSliceRef]:
675
refs: list[EvidenceSliceRef] = []
676
output_dir = manifest_path.parent / "evidence-slices" if manifest_path else None
677
if output_dir:
687
EvidenceSliceRef(
688
name=name,
689
path=path.as_posix() if path else None,
597
- item_count=len(payload.get("records", [])) if isinstance(payload.get("records"), list) else 0,
690
+ item_count=len(payload.get("records", []))
691
+ if isinstance(payload.get("records"), list)
692
+ else 0,
693
bytes=len(text.encode("utf-8")),
694
token_estimate=estimate_tokens(text),
695
checksum_sha256=checksum,
601
- provenance=payload.get("provenance", {}) if isinstance(payload.get("provenance"), dict) else {},
696
+ provenance=payload.get("provenance", {})
697
+ if isinstance(payload.get("provenance"), dict)
698
+ else {},
699
validation_errors=validate_evidence_slice(payload),
700
)
701
)
729
candidates: list[Path] = []
730
if configured:
731
configured_path = Path(configured)
635
- candidates.append(configured_path if configured_path.is_absolute() else ROOT / configured_path)
732
+ candidates.append(
733
+ configured_path if configured_path.is_absolute() else ROOT / configured_path
734
+ )
735
if not configured_path.is_absolute():
736
candidates.append(ROOT / ".squad" / configured_path)
737
candidates.append(fallback)
783
return "_No learned wisdom has been recorded yet._"
784
# Sanitize boundary markers to prevent fence escape from prior LLM output
785
from scripts.sanitize_repo_content import _escape_untrusted_boundaries
786
+
787
return _escape_untrusted_boundaries(content)
788
789
829
decisions = {"new_repos": "included", "trending_repos": "included"}
830
new_repos = payload.get("new_repos")
831
if isinstance(new_repos, list) and len(new_repos) > COMPACTED_NEW_REPOS_LIMIT:
732
- compacted["new_repos"] = _sort_repos_for_compaction(new_repos, "stars")[:COMPACTED_NEW_REPOS_LIMIT]
832
+ compacted["new_repos"] = _sort_repos_for_compaction(new_repos, "stars")[
833
+ :COMPACTED_NEW_REPOS_LIMIT
834
+ ]
835
decisions["new_repos"] = f"compacted to top {COMPACTED_NEW_REPOS_LIMIT} repos by stars"
836
trending_repos = payload.get("trending_repos")
837
if isinstance(trending_repos, list) and len(trending_repos) > COMPACTED_TRENDING_REPOS_LIMIT:
838
compacted["trending_repos"] = _sort_repos_for_compaction(trending_repos, "stars_gained")[
839
:COMPACTED_TRENDING_REPOS_LIMIT
840
]
739
- decisions["trending_repos"] = f"compacted to top {COMPACTED_TRENDING_REPOS_LIMIT} repos by stars_gained/stars"
841
+ decisions["trending_repos"] = (
842
+ f"compacted to top {COMPACTED_TRENDING_REPOS_LIMIT} repos by stars_gained/stars"
843
+ )
844
if decisions["new_repos"] != "included" or decisions["trending_repos"] != "included":
845
compacted["_preflight_compaction"] = {
846
"reason": "Rendered prompt exceeded explicit token budget before model invocation.",
847
"new_repos_original_count": len(new_repos) if isinstance(new_repos, list) else 0,
744
- "trending_repos_original_count": len(trending_repos) if isinstance(trending_repos, list) else 0,
848
+ "trending_repos_original_count": len(trending_repos)
849
+ if isinstance(trending_repos, list)
850
+ else 0,
851
"new_repos_decision": decisions["new_repos"],
852
"trending_repos_decision": decisions["trending_repos"],
853
}
911
sections.append(f"## Press Context\n\n{press_content}")
912
if historical_context_content:
913
sections.append(f"## Historical Context\n\n{historical_context_content}")
808
- if continuity_content and continuity_content != "_No continuity capsule has been recorded yet._":
914
+ if (
915
+ continuity_content
916
+ and continuity_content != "_No continuity capsule has been recorded yet._"
917
+ ):
918
sections.append(f"## Continuity Notes\n\n{continuity_content}")
919
920
return "\n\n---\n\n".join(sections)
945
946
historical_context_content = _escape_untrusted_boundaries(historical_context_content)
947
if not historical_context_content:
839
- historical_context_content = "_No historical context was available beyond the current weekly payload._"
948
+ historical_context_content = (
949
+ "_No historical context was available beyond the current weekly payload._"
950
+ )
951
952
continuity_content = render_continuity(continuity_file)
953
954
press_content = (
955
press_context_path.read_text(encoding="utf-8").strip()
845
- if press_context_path and press_context_path.exists() and press_context_path.stat().st_size > 0
956
+ if press_context_path
957
+ and press_context_path.exists()
958
+ and press_context_path.stat().st_size > 0
959
else ""
960
)
961
1019
1020
historical_context_content = _escape_untrusted_boundaries(historical_context_content)
1021
if not historical_context_content:
909
- historical_context_content = "_No historical context was available beyond the current weekly payload._"
1022
+ historical_context_content = (
1023
+ "_No historical context was available beyond the current weekly payload._"
1024
+ )
1025
1026
continuity_content = render_continuity(continuity_file)
1027
1028
press_content = (
1029
press_context_path.read_text(encoding="utf-8").strip()
915
- if press_context_path and press_context_path.exists() and press_context_path.stat().st_size > 0
1030
+ if press_context_path
1031
+ and press_context_path.exists()
1032
+ and press_context_path.stat().st_size > 0
1033
else ""
1034
)
1035
1038
press_content = _strip_ai_instruction_blocks(press_content)
1039
# Escape boundary markers in untrusted press content
1040
from scripts.sanitize_repo_content import _escape_untrusted_boundaries
1041
+
1042
if press_content:
1043
press_content = _escape_untrusted_boundaries(press_content)
1044
1080
1081
# Inject canary token for output leak detection
1082
from scripts.canary_token import generate_canary, inject_canary
1083
+
1084
canary = generate_canary()
1085
prompt = inject_canary(prompt, canary)
1086
1123
except error.HTTPError as exc:
1124
if exc.code not in RETRYABLE_STATUS_CODES or attempt == MAX_RETRIES:
1125
detail = exc.read().decode("utf-8", errors="replace")
1007
- raise RuntimeError(
1008
- f"Synthesis API request failed ({exc.code}): {detail}"
1009
- ) from exc
1126
+ raise RuntimeError(f"Synthesis API request failed ({exc.code}): {detail}") from exc
1127
# Respect Retry-After header on 429
1128
retry_after = None
1129
if exc.code == 429:
1133
retry_after = float(retry_after_header)
1134
except (ValueError, TypeError):
1135
pass
1019
- delay = retry_after if retry_after else BASE_DELAY ** (attempt + 1) + _JITTER_RANDOM.uniform(0, 1)
1136
+ delay = (
1137
+ retry_after
1138
+ if retry_after
1139
+ else BASE_DELAY ** (attempt + 1) + _JITTER_RANDOM.uniform(0, 1)
1140
+ )
1141
print(
1142
f"[retry] Synthesis API returned {exc.code}, "
1143
f"retrying in {delay:.1f}s (attempt {attempt + 1}/{MAX_RETRIES})",
1147
time.sleep(delay)
1148
except error.URLError as exc:
1149
if attempt == MAX_RETRIES:
1029
- raise RuntimeError(
1030
- f"Synthesis API network error: {exc.reason}"
1031
- ) from exc
1150
+ raise RuntimeError(f"Synthesis API network error: {exc.reason}") from exc
1151
delay = BASE_DELAY ** (attempt + 1) + _JITTER_RANDOM.uniform(0, 1)
1152
print(
1153
f"[retry] Synthesis API network error: {exc.reason}, "
1180
sanitized_payload = sanitize_repo_payload(payload)
1181
current_week = sanitized_payload["week"]
1182
previous_summary_path = find_previous_summary(current_week, analyzed_dir)
1064
- previous_summary_content = previous_summary_path.read_text(encoding="utf-8") if previous_summary_path else ""
1183
+ previous_summary_content = (
1184
+ previous_summary_path.read_text(encoding="utf-8") if previous_summary_path else ""
1185
+ )
1186
historical_context_content = assemble_historical_context(
1187
current_datetime=current_datetime,
1188
previous_summary_path=previous_summary_path,
1195
historical_context_content = _escape_untrusted_boundaries(historical_context_content)
1196
previous_summary_content = _escape_untrusted_boundaries(previous_summary_content)
1197
if not historical_context_content:
1077
- historical_context_content = "_No historical context was available beyond the current weekly payload._"
1198
+ historical_context_content = (
1199
+ "_No historical context was available beyond the current weekly payload._"
1200
+ )
1201
wisdom_content = render_wisdom(wisdom_file)
1202
skills_content = render_skills(skills_dir)
1203
continuity_content = render_continuity(continuity_file)
1204
press_content = (
1205
press_context_path.read_text(encoding="utf-8").strip()
1083
- if press_context_path and press_context_path.exists() and press_context_path.stat().st_size > 0
1206
+ if press_context_path
1207
+ and press_context_path.exists()
1208
+ and press_context_path.stat().st_size > 0
1209
else ""
1210
)
1211
# When a synthesis narrative is available (Step 1 output), it replaces
1212
# the raw press context and historical context — those were already
1213
# distilled into the narrative. This dramatically reduces token count.
1214
if synthesis_narrative:
1090
- historical_context_content = (
1091
- f"[Industry narrative synthesized from press & historical context]\n\n{synthesis_narrative}"
1092
- )
1215
+ historical_context_content = f"[Industry narrative synthesized from press & historical context]\n\n{synthesis_narrative}"
1216
press_content = ""
1217
payload_for_prompt = sanitized_payload
1218
raw_decisions = {"new_repos": "included", "trending_repos": "included"}
1219
previous_decision = "included" if previous_summary_path else "not included: no previous summary"
1220
historical_context_decision = (
1221
"included"
1099
- if historical_context_content != "_No historical context was available beyond the current weekly payload._"
1222
+ if historical_context_content
1223
+ != "_No historical context was available beyond the current weekly payload._"
1224
else "not included: no historical sources available"
1225
)
1102
- wisdom_decision = "included" if wisdom_file.exists() else "not included: no analysis-specific wisdom file"
1103
- skills_decision = "included" if skills_dir.exists() and iter_skill_files(skills_dir) else "not included: no analysis-specific skills"
1104
- continuity_decision = "included" if continuity_file.exists() else "not included: no analysis-specific continuity capsule"
1226
+ wisdom_decision = (
1227
+ "included" if wisdom_file.exists() else "not included: no analysis-specific wisdom file"
1228
+ )
1229
+ skills_decision = (
1230
+ "included"
1231
+ if skills_dir.exists() and iter_skill_files(skills_dir)
1232
+ else "not included: no analysis-specific skills"
1233
+ )
1234
+ continuity_decision = (
1235
+ "included"
1236
+ if continuity_file.exists()
1237
+ else "not included: no analysis-specific continuity capsule"
1238
+ )
1239
press_decision = "included" if press_content else "not included: no press context"
1240
degraded = False
1241
1243
raw_json_content = json.dumps(payload_for_prompt, indent=2, ensure_ascii=False)
1244
current_year, _, week_number = current_week.partition("-W")
1245
generic_title_example = (
1112
- f"Week {int(week_number)}, {current_year} Analysis" if week_number.isdigit() else "Week NN, YYYY Analysis"
1246
+ f"Week {int(week_number)}, {current_year} Analysis"
1247
+ if week_number.isdigit()
1248
+ else "Week NN, YYYY Analysis"
1249
)
1250
prompt = prompt_template_path.read_text(encoding="utf-8")
1251
replacements = {
1254
"{{CURRENT_YEAR}}": current_year,
1255
"{{TITLE_TEMPLATE_HINT}}": (
1256
f"Specific editorial headline about {current_week}'s dominant themes "
1121
- f"(not \"{generic_title_example}\")"
1257
+ f'(not "{generic_title_example}")'
1258
),
1259
"{{RAW_JSON_PATH}}": str(raw_json_path),
1260
"{{OUTPUT_PATH}}": str(output_path),
1125
- "{{PREVIOUS_SUMMARY_PATH_OR_NONE}}": str(previous_summary_path) if previous_summary_path else "None",
1261
+ "{{PREVIOUS_SUMMARY_PATH_OR_NONE}}": str(previous_summary_path)
1262
+ if previous_summary_path
1263
+ else "None",
1264
"{{HISTORICAL_CONTEXT}}": historical_context_content,
1265
"{{RAW_JSON_CONTENT}}": raw_json_content,
1266
"{{PREVIOUS_SUMMARY_CONTENT_OR_EMPTY}}": previous_summary_content.strip(),
1287
COMPACTED_HISTORICAL_CONTEXT_CHARS,
1288
"historical context",
1289
)
1152
- wisdom_content, wisdom_decision = truncate_with_notice(wisdom_content, COMPACTED_WISDOM_CHARS, "analysis wisdom")
1153
- skills_content, skills_decision = truncate_with_notice(skills_content, COMPACTED_SKILLS_CHARS, "analysis skills")
1290
+ wisdom_content, wisdom_decision = truncate_with_notice(
1291
+ wisdom_content, COMPACTED_WISDOM_CHARS, "analysis wisdom"
1292
+ )
1293
+ skills_content, skills_decision = truncate_with_notice(
1294
+ skills_content, COMPACTED_SKILLS_CHARS, "analysis skills"
1295
+ )
1296
continuity_content, continuity_decision = truncate_with_notice(
1297
continuity_content, COMPACTED_CONTINUITY_CHARS, "analysis continuity"
1298
)
1314
),
1315
_component(
1316
name="new_repos",
1175
- content=json.dumps(payload_for_prompt.get("new_repos", []), indent=2, ensure_ascii=False),
1317
+ content=json.dumps(
1318
+ payload_for_prompt.get("new_repos", []), indent=2, ensure_ascii=False
1319
+ ),
1320
path=raw_json_path,
1321
included=True,
1322
inclusion_reason="Deterministic mapper slice: newly discovered repositories.",
1324
),
1325
_component(
1326
name="trending_repos",
1183
- content=json.dumps(payload_for_prompt.get("trending_repos", []), indent=2, ensure_ascii=False),
1327
+ content=json.dumps(
1328
+ payload_for_prompt.get("trending_repos", []), indent=2, ensure_ascii=False
1329
+ ),
1330
path=raw_json_path,
1331
included=True,
1332
inclusion_reason="Deterministic mapper slice: continuing/trending repositories.",
1338
path=raw_json_path,
1339
included=True,
1340
inclusion_reason=f"Sanitized current weekly payload for {current_year}-W{week_number}.",
1195
- compaction_decision="included" if not degraded else "included with compacted repo slices",
1341
+ compaction_decision="included"
1342
+ if not degraded
1343
+ else "included with compacted repo slices",
1344
),
1345
_component(
1346
name="prior_continuity",
1396
path=None,
1397
included=True,
1398
inclusion_reason="Exact prompt that will be passed to Copilot CLI.",
1251
- compaction_decision="included" if not degraded else "included after deterministic compaction",
1399
+ compaction_decision="included"
1400
+ if not degraded
1401
+ else "included after deterministic compaction",
1402
),
1403
]
1404
prompt_tokens = estimate_tokens(prompt)
1405
prompt_within_budget = prompt_tokens <= prompt_token_budget
1406
degradation_reason = (
1257
- "Prompt was deterministically compacted to fit the configured token budget." if degraded else None
1407
+ "Prompt was deterministically compacted to fit the configured token budget."
1408
+ if degraded
1409
+ else None
1410
)
1411
evidence_slices = build_evidence_slices(
1412
week=current_week,
1419
previous_summary_content=previous_summary_content,
1420
)
1421
news_path, corr_path = _press_paths_for_context(press_context_path, current_week)
1270
- press_inventory = _article_inventory(_safe_load_json(news_path), _safe_load_json(corr_path), news_path)
1422
+ press_inventory = _article_inventory(
1423
+ _safe_load_json(news_path), _safe_load_json(corr_path), news_path
1424
+ )
1425
slice_refs = write_evidence_slices(evidence_slices, None)
1426
preflight = PromptPreflight(
1427
schema_version="analysis_input_manifest_v1",
1448
"degraded/compacted prompts are staged/candidate-only by default."
1449
),
1450
components=components,
1297
- deterministic_slices=["new_repos", "trending_repos", "press_correlations", "prior_continuity"],
1451
+ deterministic_slices=[
1452
+ "new_repos",
1453
+ "trending_repos",
1454
+ "press_correlations",
1455
+ "prior_continuity",
1456
+ ],
1457
generated_evidence_slices=slice_refs,
1458
evidence_slice_payloads=evidence_slices,
1459
evidence_inventories=[
1460
_evidence_inventory("raw_new_repos", sanitized_payload, "new_repos", raw_json_path),
1302
- _evidence_inventory("raw_trending_repos", sanitized_payload, "trending_repos", raw_json_path),
1461
+ _evidence_inventory(
1462
+ "raw_trending_repos", sanitized_payload, "trending_repos", raw_json_path
1463
+ ),
1464
_evidence_inventory("prompt_new_repos", payload_for_prompt, "new_repos", raw_json_path),
1304
- _evidence_inventory("prompt_trending_repos", payload_for_prompt, "trending_repos", raw_json_path),
1465
+ _evidence_inventory(
1466
+ "prompt_trending_repos", payload_for_prompt, "trending_repos", raw_json_path
1467
+ ),
1468
],
1469
press_inventories=[press_inventory],
1470
)
1506
return prompt
1507
1508
1346
-def write_preflight_reports(preflight: PromptPreflight, json_path: Path | None, md_path: Path | None) -> None:
1509
+def write_preflight_reports(
1510
+ preflight: PromptPreflight, json_path: Path | None, md_path: Path | None
1511
+) -> None:
1512
if json_path and preflight.evidence_slice_payloads:
1348
- preflight.generated_evidence_slices = write_evidence_slices(preflight.evidence_slice_payloads, json_path)
1513
+ preflight.generated_evidence_slices = write_evidence_slices(
1514
+ preflight.evidence_slice_payloads, json_path
1515
+ )
1516
if json_path:
1517
json_path.parent.mkdir(parents=True, exist_ok=True)
1351
- json_path.write_text(json.dumps(asdict(preflight), indent=2, sort_keys=True) + "\n", encoding="utf-8")
1518
+ json_path.write_text(
1519
+ json.dumps(asdict(preflight), indent=2, sort_keys=True) + "\n", encoding="utf-8"
1520
+ )
1521
if md_path:
1522
md_path.parent.mkdir(parents=True, exist_ok=True)
1523
rows = [
1584
raise ValueError("GitHub Models response did not contain markdown output.")
1585
1586
1418
-def validate_https_url(url: str, *, label: str, allowed_hosts: frozenset[str] | None = None) -> None:
1587
+def validate_https_url(
1588
+ url: str, *, label: str, allowed_hosts: frozenset[str] | None = None
1589
+) -> None:
1590
parsed = parse.urlparse(url)
1591
if parsed.scheme.lower() != "https":
1592
raise ValueError(f"{label} must use HTTPS: {url}")
1609
1610
Returns a list of security violation messages (empty = safe).
1611
"""
1441
- from scripts.canary_token import check_output_for_leak, check_output_for_any_canary
1612
+ from scripts.canary_token import check_output_for_any_canary, check_output_for_leak
1613
1614
violations: list[str] = []
1615
1631
)
1632
1633
# Check for boundary marker leaks (model reproduced internal framing)
1463
- from scripts.sanitize_repo_content import BOUNDARY_OPEN, BOUNDARY_CLOSE
1634
+ from scripts.sanitize_repo_content import BOUNDARY_CLOSE, BOUNDARY_OPEN
1635
+
1636
if BOUNDARY_OPEN in output:
1637
violations.append(
1638
"Output contains <untrusted-content> boundary marker — "
1654
1655
# Inject canary token for output leak detection
1656
from scripts.canary_token import generate_canary, inject_canary
1657
+
1658
canary = generate_canary()
1659
prompt = inject_canary(prompt, canary)
1660
1701
detail = exc.read().decode("utf-8", errors="replace")
1702
retry_class = (
1703
"non-retryable"
1531
- if exc.code in NON_RETRYABLE_STATUS_CLASSES or exc.code not in RETRYABLE_STATUS_CODES
1704
+ if exc.code in NON_RETRYABLE_STATUS_CLASSES
1705
+ or exc.code not in RETRYABLE_STATUS_CODES
1706
else "retry-exhausted"
1707
)
1534
- access_hint = " GitHub Models access is unavailable for this model." if exc.code == 403 else ""
1708
+ access_hint = (
1709
+ " GitHub Models access is unavailable for this model."
1710
+ if exc.code == 403
1711
+ else ""
1712
+ )
1713
raise RuntimeError(
1714
f"GitHub Models API request failed ({exc.code}, {retry_class}): {detail}{access_hint}"
1715
) from exc
1716
# Determine delay: respect Retry-After header on 429
1539
- retry_after = exc.headers.get("Retry-After") if exc.code == 429 and exc.headers is not None else None
1717
+ retry_after = (
1718
+ exc.headers.get("Retry-After")
1719
+ if exc.code == 429 and exc.headers is not None
1720
+ else None
1721
+ )
1722
if retry_after is not None:
1723
try:
1724
delay = float(retry_after)
1737
time.sleep(total_delay)
1738
except error.URLError as exc:
1739
if attempt == MAX_RETRIES:
1558
- raise RuntimeError(
1559
- f"GitHub Models API request failed: {exc.reason}"
1560
- ) from exc
1740
+ raise RuntimeError(f"GitHub Models API request failed: {exc.reason}") from exc
1741
delay = BASE_DELAY ** (attempt + 1) + _JITTER_RANDOM.uniform(0, 1)
1742
print(
1743
f"[retry] GitHub Models API network error: {exc.reason}, "
1793
truncated = "\n".join(list_lines[:10])
1794
truncated += f"\n…and {omitted} more repos with press correlation\n"
1795
content = (
1616
- content[: corr_match.start()]
1617
- + header
1618
- + truncated
1619
- + content[corr_match.end() :]
1796
+ content[: corr_match.start()] + header + truncated + content[corr_match.end() :]
1797
)
1798
1799
# Truncate divergence lists to top 10 items each
1815
truncated = "\n".join(list_lines[:10])
1816
truncated += f"\n- …and {omitted} more topics\n"
1817
content = (
1641
- content[: div_match.start()]
1642
- + header
1643
- + truncated
1644
- + content[div_match.end() :]
1818
+ content[: div_match.start()] + header + truncated + content[div_match.end() :]
1819
)
1820
1821
# Add reader-friendly conclusion if divergences exist but instructions were stripped
1832
1833
def _render_press_section_no_ai(press_context_path: Path | None) -> str:
1834
"""Render press context data for the no-AI summary (reader-facing)."""
1661
- if not press_context_path or not press_context_path.exists() or press_context_path.stat().st_size == 0:
1835
+ if (
1836
+ not press_context_path
1837
+ or not press_context_path.exists()
1838
+ or press_context_path.stat().st_size == 0
1839
+ ):
1840
return (
1841
"No industry press data was available for this week's analysis. "
1842
"Future runs with TechCrunch integration enabled will provide "
1855
corr_path = data_dir / "analyzed" / f"{week}-correlations.json"
1856
1857
if tc_path.exists():
1680
- from scripts.render_press_context import render_press_context, load_json as rpc_load_json
1858
+ from scripts.render_press_context import load_json as rpc_load_json
1859
+ from scripts.render_press_context import render_press_context
1860
+
1861
tc_data = rpc_load_json(tc_path)
1862
corr_data = rpc_load_json(corr_path) if corr_path.exists() else {}
1863
if tc_data is not None:
1868
return _strip_ai_instructions(content)
1869
1870
1691
-def generate_no_ai_summary(raw_json_path: Path, current_datetime: str, press_context_path: Path | None = None) -> str:
1871
+def generate_no_ai_summary(
1872
+ raw_json_path: Path, current_datetime: str, press_context_path: Path | None = None
1873
+) -> str:
1874
"""Generate a valid summary from raw JSON without any AI API calls."""
1875
payload = sanitize_repo_payload(load_json(raw_json_path))
1876
week = payload["week"]
1886
all_repos = sorted(new_repos + trending_repos, key=lambda r: r.get("stars", 0), reverse=True)
1887
top_repo = all_repos[0]["full_name"] if all_repos else "unknown/unknown"
1888
1707
- tags = top_topics[:5] if len(top_topics) >= 3 else ["open-source", "developer-tools", "automation"]
1889
+ tags = (
1890
+ top_topics[:5] if len(top_topics) >= 3 else ["open-source", "developer-tools", "automation"]
1891
+ )
1892
1893
# Notable new repos
1894
notable_new = sorted(new_repos, key=lambda r: r.get("stars", 0), reverse=True)[:10]
1900
f"- [{repo['full_name']}]({repo.get('url', '#')}) ({lang}, "
1901
f"{repo.get('stars', 0):,} stars): {desc}"
1902
)
1719
- notable_section = "\n".join(notable_lines) if notable_lines else "No new repositories were captured this week."
1720
-
1721
- # Trending repos
1722
- top_trending = sorted(trending_repos, key=lambda r: r.get("stars", 0), reverse=True)[:10]
1723
- trending_lines = []
1724
- for repo in top_trending:
1725
- desc = repo.get("description") or "No description provided"
1726
- lang = repo.get("language") or "Unknown"
1727
- trending_lines.append(
1728
- f"- [{repo['full_name']}]({repo.get('url', '#')}) ({lang}, "
1729
- f"{repo.get('stars', 0):,} stars): {desc}"
1730
- )
1731
- trending_section = "\n".join(trending_lines) if trending_lines else "No trending repositories were captured this week."
1903
+ notable_section = (
1904
+ "\n".join(notable_lines)
1905
+ if notable_lines
1906
+ else "No new repositories were captured this week."
1907
+ )
1908
1909
# Language breakdown
1910
lang_counts: dict[str, int] = {}
1913
if lang:
1914
lang_counts[lang] = lang_counts.get(lang, 0) + 1
1915
top_langs = sorted(lang_counts.items(), key=lambda x: x[1], reverse=True)[:5]
1740
- lang_summary = ", ".join(f"{lang} ({count})" for lang, count in top_langs) if top_langs else "diverse mix of languages"
1916
+ lang_summary = (
1917
+ ", ".join(f"{lang} ({count})" for lang, count in top_langs)
1918
+ if top_langs
1919
+ else "diverse mix of languages"
1920
+ )
1921
1922
year_str = week.split("-W")[0]
1923
week_num = week.split("-W")[1]
2010
prompt_token_budget=args.prompt_token_budget,
2011
)
2012
if not narrative_or_prompt:
1833
- print("::warning::No meaningful content for synthesis (no press or historical context).", file=sys.stderr)
2013
+ print(
2014
+ "::warning::No meaningful content for synthesis (no press or historical context).",
2015
+ file=sys.stderr,
2016
+ )
2017
return 1
2018
output_path = args.synthesis_output or args.output
2019
output_path.parent.mkdir(parents=True, exist_ok=True)
2026
2027
# Load synthesis narrative from Step 1 output if provided
2028
synthesis_narrative: str | None = None
1846
- if args.synthesis_input and args.synthesis_input.exists() and args.synthesis_input.stat().st_size > 0:
2029
+ if (
2030
+ args.synthesis_input
2031
+ and args.synthesis_input.exists()
2032
+ and args.synthesis_input.stat().st_size > 0
2033
+ ):
2034
synthesis_narrative = args.synthesis_input.read_text(encoding="utf-8").strip()
2035
if synthesis_narrative:
2036
# Escape boundary markers — synthesis output is untrusted LLM content
2037
from scripts.sanitize_repo_content import _escape_untrusted_boundaries
2038
+
2039
synthesis_narrative = _escape_untrusted_boundaries(synthesis_narrative)
2040
print(
2041
f"::notice::Using synthesis narrative ({estimate_tokens(synthesis_narrative)} tokens) from {args.synthesis_input}",