"""Deterministic JSON CLI for inspection and explicit derived-output integration.""" from __future__ import annotations import argparse import json import sys import webbrowser from pathlib import Path from .application import CanonicalApplicationService, GenericCanonicalApplier from .context import compile_context from .errors import DocForgeError from .index import ProjectIndex from .project import Project, project_root_fingerprint from .rendering import RenderService from .viewer_manager import ViewerManagerClient 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("reindex") commands.add_parser("sync") 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) render = commands.add_parser("render") render.add_argument("view_id") render_status = commands.add_parser("render-status") render_status.add_argument("view_id", nargs="?") preview = commands.add_parser("preview") preview.add_argument("changeset_id") preview.add_argument("view_id") apply_command = commands.add_parser("apply") apply_command.add_argument("changeset_id") apply_command.add_argument("--changeset-hash", required=True) apply_command.add_argument("--applier", required=True) visualize = commands.add_parser("visualize") target = visualize.add_mutually_exclusive_group() target.add_argument("--node") target.add_argument("--query") visualize.add_argument("--depth", type=int, default=1) visualize.add_argument("--no-open", action="store_true") commands.add_parser("visualization-status") commands.add_parser("visualization-stop") 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 == "reindex": built = index.build() return { **built, "reindexed": True, "check": index.check(), } if arguments.command == "sync": return index.synchronize() 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) if arguments.command == "render": return RenderService(project).render(arguments.view_id) if arguments.command == "render-status": return RenderService(project).status(arguments.view_id) if arguments.command == "preview": return RenderService(project).preview(arguments.changeset_id, arguments.view_id) if arguments.command == "apply": return CanonicalApplicationService( project, applier_id=arguments.applier, applier=GenericCanonicalApplier(project), ).apply(arguments.changeset_id, arguments.changeset_hash) if arguments.command == "visualize": visualization = ViewerManagerClient(index).start( node_id=arguments.node, query=arguments.query, depth=arguments.depth, ) opened = False if not arguments.no_open: opened = webbrowser.open(str(visualization["url"])) return { "status": "ok", "project_id": project.descriptor.project_id, "project_root_fingerprint": project_root_fingerprint(project.descriptor.root), "adapter": project.descriptor.adapter, "opened_browser": opened, "visualization": visualization, } if arguments.command == "visualization-status": return ViewerManagerClient(index).status() if arguments.command == "visualization-stop": return ViewerManagerClient(index).stop() 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())