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>
183 lines
6.4 KiB
Python
183 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Reference Discovery CLI — manage source discovery and manifests.
|
|
|
|
Claude performs the actual web search. This script handles:
|
|
- Receiving search results and creating manifests
|
|
- Deduplicating URLs against the existing repository
|
|
- Registering new sources in the database
|
|
|
|
Usage:
|
|
python discover.py create-manifest --topic "prompt engineering" --output manifest.json < urls.json
|
|
python discover.py dedup --manifest manifest.json [--output deduped.json]
|
|
python discover.py register-source --name "Anthropic Docs" --type official_docs --url "https://docs.anthropic.com" --tier tier1_official --vendor anthropic
|
|
python discover.py list-sources [--vendor anthropic]
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
import click
|
|
from rich.console import Console
|
|
from rich.table import Table
|
|
|
|
from refcurator.db import db_session
|
|
from refcurator.manifest import write_manifest, read_manifest, dedup_manifest_urls
|
|
from refcurator.models import Manifest, ManifestURL
|
|
from refcurator.utils import normalize_url
|
|
|
|
console = Console()
|
|
|
|
|
|
@click.group()
|
|
def cli():
|
|
"""Reference Discovery — manage source manifests."""
|
|
pass
|
|
|
|
|
|
@cli.command("create-manifest")
|
|
@click.option("--topic", required=True, help="Discovery topic")
|
|
@click.option("--output", required=True, type=click.Path(), help="Output manifest path")
|
|
@click.option("--input", "input_file", type=click.Path(exists=True),
|
|
help="Input JSON file with URL entries (or pipe via stdin)")
|
|
def create_manifest(topic, output, input_file):
|
|
"""Create a manifest from discovered URLs.
|
|
|
|
Input format (JSON array):
|
|
[
|
|
{"url": "https://...", "title": "...", "credibility_score": 0.85, "source_type": "official_docs"},
|
|
...
|
|
]
|
|
"""
|
|
if input_file:
|
|
data = json.loads(Path(input_file).read_text())
|
|
elif not sys.stdin.isatty():
|
|
data = json.load(sys.stdin)
|
|
else:
|
|
console.print("[red]Error:[/red] Provide URLs via --input file or stdin")
|
|
sys.exit(1)
|
|
|
|
urls = [ManifestURL(**entry) for entry in data]
|
|
|
|
# Normalize and deduplicate
|
|
seen = {}
|
|
for u in urls:
|
|
norm = normalize_url(u.url)
|
|
if norm not in seen or (u.credibility_score or 0) > (seen[norm].credibility_score or 0):
|
|
seen[norm] = u
|
|
|
|
unique_urls = list(seen.values())
|
|
|
|
manifest = Manifest(
|
|
discovery_date=datetime.now().isoformat(),
|
|
topic=topic,
|
|
total_urls=len(unique_urls),
|
|
urls=unique_urls,
|
|
)
|
|
|
|
out_path = Path(output)
|
|
write_manifest(manifest, out_path)
|
|
console.print(f"[green]Created manifest:[/green] {len(unique_urls)} URLs → {output}")
|
|
|
|
|
|
@cli.command()
|
|
@click.option("--manifest", required=True, type=click.Path(exists=True), help="Manifest to dedup")
|
|
@click.option("--output", type=click.Path(), help="Output path (defaults to overwriting input)")
|
|
def dedup(manifest, output):
|
|
"""Deduplicate manifest URLs against existing repository."""
|
|
m = read_manifest(Path(manifest))
|
|
original_count = len(m.urls)
|
|
|
|
with db_session() as db:
|
|
existing = db.fetch_all("SELECT url FROM documents WHERE url IS NOT NULL")
|
|
existing_urls = {r["url"] for r in existing}
|
|
|
|
deduped = dedup_manifest_urls(m, existing_urls)
|
|
removed = original_count - len(deduped.urls)
|
|
|
|
out_path = Path(output) if output else Path(manifest)
|
|
write_manifest(deduped, out_path)
|
|
|
|
console.print(
|
|
f"[green]Deduped:[/green] {original_count} → {len(deduped.urls)} URLs "
|
|
f"({removed} duplicates removed)"
|
|
)
|
|
|
|
|
|
@cli.command("register-source")
|
|
@click.option("--name", required=True, help="Source name")
|
|
@click.option("--type", "source_type", required=True,
|
|
type=click.Choice(["official_docs", "engineering_blog", "research_paper",
|
|
"github_repo", "community_guide", "pdf_document", "api_reference"]))
|
|
@click.option("--url", required=True, help="Base URL")
|
|
@click.option("--tier", default="tier3_community",
|
|
type=click.Choice(["tier1_official", "tier2_verified", "tier3_community"]))
|
|
@click.option("--vendor", help="Vendor name (e.g., anthropic, openai)")
|
|
def register_source(name, source_type, url, tier, vendor):
|
|
"""Register a new source in the repository."""
|
|
with db_session() as db:
|
|
# Check for existing source with same base_url
|
|
existing = db.fetch_one(
|
|
"SELECT source_id, source_name FROM sources WHERE base_url = %s",
|
|
(url,),
|
|
)
|
|
if existing:
|
|
console.print(
|
|
f"[yellow]Already registered:[/yellow] source_id={existing['source_id']} "
|
|
f"({existing['source_name']})"
|
|
)
|
|
return
|
|
|
|
source_id = db.insert_returning_id(
|
|
"""INSERT INTO sources (source_name, source_type, base_url, credibility_tier, vendor)
|
|
VALUES (%s, %s, %s, %s, %s)""",
|
|
(name, source_type, url, tier, vendor),
|
|
)
|
|
|
|
console.print(f"[green]Registered:[/green] source_id={source_id} — {name}")
|
|
click.echo(json.dumps({"source_id": source_id, "name": name, "url": url}))
|
|
|
|
|
|
@cli.command("list-sources")
|
|
@click.option("--vendor", help="Filter by vendor")
|
|
@click.option("--tier", type=click.Choice(["tier1_official", "tier2_verified", "tier3_community"]),
|
|
help="Filter by credibility tier")
|
|
def list_sources(vendor, tier):
|
|
"""List registered sources."""
|
|
with db_session() as db:
|
|
if vendor:
|
|
rows = db.fetch_all(
|
|
"SELECT * FROM sources WHERE vendor = %s ORDER BY credibility_tier",
|
|
(vendor,),
|
|
)
|
|
elif tier:
|
|
rows = db.fetch_all(
|
|
"SELECT * FROM sources WHERE credibility_tier = %s ORDER BY vendor",
|
|
(tier,),
|
|
)
|
|
else:
|
|
rows = db.fetch_all("SELECT * FROM sources ORDER BY credibility_tier, vendor")
|
|
|
|
table = Table(title="Registered Sources")
|
|
table.add_column("ID", style="cyan")
|
|
table.add_column("Name")
|
|
table.add_column("Type")
|
|
table.add_column("Tier")
|
|
table.add_column("Vendor")
|
|
table.add_column("URL")
|
|
for r in rows:
|
|
table.add_row(
|
|
str(r.get("source_id", "")),
|
|
str(r.get("source_name", "")),
|
|
str(r.get("source_type", "")),
|
|
str(r.get("credibility_tier", "")),
|
|
str(r.get("vendor", "")),
|
|
str(r.get("base_url", ""))[:40],
|
|
)
|
|
console.print(table)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
cli()
|