feat(reference-curator): implement Python scripts + Gemini quality gate
Build the refcurator shared Python package and 7 CLI scripts that were previously specification-only. Add Gemini CLI as an independent pre-distillation quality evaluator, replacing the circular Claude-self-review pattern. Key changes: - shared/lib/src/refcurator/: 7-module package (config, db, models, utils, manifest, gemini) with PyMySQL + JSON file dual backend - 7 Click CLI scripts: discover, crawl_mgr, repo, distiller, reviewer, exporter, pipeline — each with subcommands for data management - Gemini quality gate: evaluates raw content BEFORE distillation using 5 criteria (relevance, authority, completeness, freshness, distill_value) - Pipeline reordered: discovery → crawl → store → evaluate → distill → export - Bug fixes from Codex adversarial review: - FileBackend now hard-fails on JOIN/aggregate/GROUP BY queries - Exporter uses MAX(review_id) to prevent shipping stale approvals - Distiller updates existing rows on refactor instead of forking - Updated all 7 CLAUDE.md directives with real script references - install.sh updated with refcurator package install step 51/51 E2E tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
"""Manifest I/O for reference discovery and crawl results."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from refcurator.models import CrawlResult, CrawlResultEntry, Manifest, ManifestURL
|
||||
from refcurator.utils import normalize_url
|
||||
|
||||
|
||||
def read_manifest(path: Path) -> Manifest:
|
||||
"""Read a manifest JSON file."""
|
||||
data = json.loads(path.read_text())
|
||||
return Manifest(**data)
|
||||
|
||||
|
||||
def write_manifest(manifest: Manifest, path: Path) -> None:
|
||||
"""Write a manifest to a JSON file."""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(manifest.model_dump_json(indent=2))
|
||||
|
||||
|
||||
def merge_manifests(manifests: list[Manifest]) -> Manifest:
|
||||
"""Merge multiple manifests, deduplicating URLs."""
|
||||
seen: dict[str, ManifestURL] = {}
|
||||
topic_parts = []
|
||||
|
||||
for m in manifests:
|
||||
if m.topic:
|
||||
topic_parts.append(m.topic)
|
||||
for url_entry in m.urls:
|
||||
normalized = normalize_url(url_entry.url)
|
||||
existing = seen.get(normalized)
|
||||
if existing is None or (
|
||||
url_entry.credibility_score
|
||||
and (existing.credibility_score or 0) < url_entry.credibility_score
|
||||
):
|
||||
seen[normalized] = url_entry
|
||||
|
||||
urls = list(seen.values())
|
||||
return Manifest(
|
||||
discovery_date=datetime.now().isoformat(),
|
||||
topic=" + ".join(topic_parts) if topic_parts else None,
|
||||
total_urls=len(urls),
|
||||
urls=urls,
|
||||
)
|
||||
|
||||
|
||||
def dedup_manifest_urls(manifest: Manifest, existing_urls: set[str]) -> Manifest:
|
||||
"""Remove URLs already in the existing set (normalized comparison)."""
|
||||
existing_normalized = {normalize_url(u) for u in existing_urls}
|
||||
filtered = [u for u in manifest.urls if normalize_url(u.url) not in existing_normalized]
|
||||
return Manifest(
|
||||
discovery_date=manifest.discovery_date,
|
||||
topic=manifest.topic,
|
||||
total_urls=len(filtered),
|
||||
urls=filtered,
|
||||
)
|
||||
|
||||
|
||||
def read_crawl_result(path: Path) -> CrawlResult:
|
||||
"""Read a crawl result JSON file."""
|
||||
data = json.loads(path.read_text())
|
||||
return CrawlResult(**data)
|
||||
|
||||
|
||||
def write_crawl_result(result: CrawlResult, path: Path) -> None:
|
||||
"""Write a crawl result to a JSON file."""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(result.model_dump_json(indent=2))
|
||||
|
||||
|
||||
def create_crawl_result(
|
||||
entries: list[dict],
|
||||
crawler: str = "firecrawl",
|
||||
) -> CrawlResult:
|
||||
"""Create a CrawlResult from a list of crawl entry dicts."""
|
||||
docs = [CrawlResultEntry(**e) for e in entries]
|
||||
completed = [d for d in docs if d.status == "completed"]
|
||||
failed = [d for d in docs if d.status != "completed"]
|
||||
|
||||
return CrawlResult(
|
||||
crawl_date=datetime.now().isoformat(),
|
||||
crawler_used=crawler,
|
||||
total_crawled=len(completed),
|
||||
total_failed=len(failed),
|
||||
documents=docs,
|
||||
)
|
||||
Reference in New Issue
Block a user