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:
2026-04-12 18:19:52 +09:00
parent 133df68b81
commit f215c11c32
23 changed files with 3917 additions and 583 deletions

View File

@@ -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,
)