21
from pathlib import Path
22
from typing import Any
23
24
+from scripts.sanitize_repo_content import sanitize_text
25
from scripts.topic_paths import analyzed_dir, raw_dir
26
27
MAX_ARTICLES_FOR_CORRELATION = 80
30
MAX_DIVERGENCE_ARTICLES = 30
31
WEAK_MATCH_TYPES = {"category", "project_name"}
32
33
+# Length caps for sanitized correlation output fields
34
+_CITATION_TITLE_MAX = 200
35
+_CITATION_URL_MAX = 300
36
+_CITATION_SOURCE_MAX = 100
37
+_REPO_NAME_MAX = 200
38
+
39
40
def log(message: str) -> None:
41
print(f"[correlate] {message}", file=sys.stderr)
202
203
204
def _article_citation(article: dict[str, Any]) -> dict[str, Any]:
198
- """Return the bounded citation fields downstream renderers are allowed to use."""
205
+ """Return bounded, sanitized citation fields for downstream renderers."""
206
return {
200
- "title": article.get("title", ""),
201
- "url": article.get("url", ""),
202
- "source": article.get("source", "unknown"),
203
- "sources": article.get("sources", [article.get("source", "unknown")]),
207
+ "title": sanitize_text(
208
+ article.get("title", ""),
209
+ max_length=_CITATION_TITLE_MAX,
210
+ label="article title",
211
+ ),
212
+ "url": sanitize_text(
213
+ article.get("url", ""),
214
+ max_length=_CITATION_URL_MAX,
215
+ label="article url",
216
+ ),
217
+ "source": sanitize_text(
218
+ article.get("source", "unknown"),
219
+ max_length=_CITATION_SOURCE_MAX,
220
+ label="article source",
221
+ ),
222
+ "sources": [
223
+ sanitize_text(s, max_length=_CITATION_SOURCE_MAX, label="article source")
224
+ for s in article.get("sources", [article.get("source", "unknown")])
225
+ ],
226
"published_at": article.get("published_at", ""),
227
"relevance_score": article.get("relevance_score", 0),
228
}
349
temporal_spike=temporal_spike,
350
)
351
352
+ raw_repo_key = repo.get("full_name") or f"{repo.get('owner')}/{repo.get('name')}"
353
+ repo_name = sanitize_text(
354
+ raw_repo_key,
355
+ max_length=_REPO_NAME_MAX,
356
+ label="correlation repo name",
357
+ )
358
+
359
return {
331
- "repo": repo.get("full_name") or f"{repo.get('owner')}/{repo.get('name')}",
360
+ "repo": repo_name,
361
+ "repo_key": raw_repo_key,
362
"press_correlated": press_correlated,
363
"correlation_confidence": round(best_confidence, 2),
364
"matched_articles": matched_articles,
442
]
443
444
# Find repos that had no correlation match
415
- correlated_repo_names: set[str] = {c.get("repo", "") for c in correlations}
445
+ correlated_repo_names: set[str] = {c.get("repo_key", c.get("repo", "")) for c in correlations}
446
unmatched_repos = [
447
r for r in repos
448
if (r.get("full_name") or f"{r.get('owner')}/{r.get('name')}") not in correlated_repo_names