"""Sync repo-managed persona/evaluator source packs into KB. Default mode is a DB-backed dry run: compare repo source pack content hashes against active kb.document rows and report the versions that would be used. Use --apply to upsert kb.source rows and index changed documents. """ from __future__ import annotations import argparse import asyncio import dataclasses import json import sys from pathlib import Path from typing import Any REPO_ROOT = Path(__file__).resolve().parents[1] API_ROOT = REPO_ROOT / "apps" / "api" if str(API_ROOT) not in sys.path: sys.path.insert(0, str(API_ROOT)) from app.db import acquire, close_pool, init_pool # noqa: E402 from app.services import source_pack_sync # noqa: E402 def _to_payload(result: source_pack_sync.RepoSourcePackSyncResult) -> dict[str, Any]: return { "applied": result.applied, "sources_upserted": result.sources_upserted, "manifest_count": result.manifest_count, "chunks_indexed": result.chunks_indexed, "skipped_unchanged": result.skipped_unchanged, "embedded": result.embedded, "degraded": result.degraded, "items": [dataclasses.asdict(item) for item in result.items], } def _print_text(result: source_pack_sync.RepoSourcePackSyncResult) -> None: mode = "APPLY" if result.applied else "DRY-RUN" print(f"persona source pack sync: {mode}") print(f"manifest_count: {result.manifest_count}") print(f"sources_upserted: {result.sources_upserted}") print(f"chunks_indexed: {result.chunks_indexed}") print(f"skipped_unchanged: {result.skipped_unchanged}") for item in result.items: previous = "-" if item.previous_version is None else str(item.previous_version) action = "skip" if item.skipped_unchanged else ("apply" if result.applied else "would_apply") print( f"- {item.source_id} {action} v{previous}->v{item.new_version} " f"{item.content_hash[:12]} chunks={item.chunks_indexed}" ) async def _main_async(args: argparse.Namespace) -> int: await init_pool() try: async with acquire(role="admin") as conn: result = await source_pack_sync.sync_repo_source_packs(conn, apply=bool(args.apply)) finally: await close_pool() if args.json: print(json.dumps(_to_payload(result), ensure_ascii=False, indent=2, sort_keys=True)) else: _print_text(result) return 0 def parse_args(argv: list[str]) -> argparse.Namespace: parser = argparse.ArgumentParser( description=( "Compare or apply repo-managed persona/evaluator source packs. " "Dry-run is the default and reads DB state without writes; --apply " "upserts source rows and indexes changed document versions." ) ) parser.add_argument("--apply", action="store_true", help="write changed source packs into KB") parser.add_argument("--json", action="store_true", help="emit machine-readable JSON") return parser.parse_args(argv) def main(argv: list[str] | None = None) -> int: args = parse_args(list(argv if argv is not None else sys.argv[1:])) return asyncio.run(_main_async(args)) if __name__ == "__main__": raise SystemExit(main())