"""Deterministic JSON command-line interface for DocForge project inspection.""" from __future__ import annotations import argparse import json import sys from pathlib import Path from .context import compile_context from .errors import DocForgeError from .index import ProjectIndex from .project import Project, project_root_fingerprint def _parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(prog="docforge") parser.add_argument("--project-root", type=Path, required=True) commands = parser.add_subparsers(dest="command", required=True) commands.add_parser("info") commands.add_parser("validate") commands.add_parser("build") commands.add_parser("check") commands.add_parser("validate-index") show = commands.add_parser("show") show.add_argument("node_id") search = commands.add_parser("search") search.add_argument("query") search.add_argument("--limit", type=int) filter_command = commands.add_parser("filter") filter_command.add_argument("--family") filter_command.add_argument("--authority") filter_command.add_argument("--status") filter_command.add_argument("--tag") filter_command.add_argument("--limit", type=int) for name in ("backlinks", "dependencies", "impact"): command = commands.add_parser(name) command.add_argument("node_id") if name == "backlinks": command.add_argument("--relation") else: command.add_argument("--depth", type=int, default=2) context = commands.add_parser("context") context.add_argument("profile") context.add_argument("--budget", type=int) return parser def _run(arguments: argparse.Namespace) -> dict[str, object]: project = Project.open(arguments.project_root) index = ProjectIndex(project) if arguments.command == "info": snapshot = project.load() return { "status": "ok", "project_id": snapshot.descriptor.project_id, "project_root_fingerprint": project_root_fingerprint(snapshot.descriptor.root), "title": snapshot.descriptor.title, "adapter": snapshot.descriptor.adapter, "revision": snapshot.revision, "source_hash": snapshot.source_hash, "node_count": len(snapshot.nodes), "edge_count": len(snapshot.edges), "index": str(snapshot.descriptor.index_path), } if arguments.command == "validate": snapshot = project.load() return { "status": "ok", "project_id": snapshot.descriptor.project_id, "project_root_fingerprint": project_root_fingerprint(snapshot.descriptor.root), "revision": snapshot.revision, "source_hash": snapshot.source_hash, "node_count": len(snapshot.nodes), "edge_count": len(snapshot.edges), } if arguments.command == "build": return index.build() if arguments.command == "check": return index.check() if arguments.command == "validate-index": return index.check() if arguments.command == "show": return index.get_node(arguments.node_id) if arguments.command == "search": return index.search(arguments.query, limit=arguments.limit) if arguments.command == "filter": return index.filter_nodes( family=arguments.family, authority=arguments.authority, status=arguments.status, tag=arguments.tag, limit=arguments.limit, ) if arguments.command == "backlinks": return index.backlinks(arguments.node_id, relation=arguments.relation) if arguments.command == "dependencies": return index.dependencies(arguments.node_id, depth=arguments.depth) if arguments.command == "impact": return index.impact(arguments.node_id, depth=arguments.depth) if arguments.command == "context": return compile_context(index, arguments.profile, arguments.budget) raise DocForgeError("invalid_command", "Unknown command") def main(argv: list[str] | None = None) -> int: parser = _parser() arguments = parser.parse_args(argv) try: result = _run(arguments) code = 0 except DocForgeError as error: result = {"status": "error", "error": error.as_dict()} code = 2 print(json.dumps(result, sort_keys=True, indent=2)) return code if __name__ == "__main__": sys.exit(main())