2026-07-22 03:32:05 -04:00
|
|
|
"""Deterministic JSON CLI for inspection and explicit derived-output integration."""
|
2026-07-22 01:29:32 -04:00
|
|
|
|
|
|
|
|
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
|
2026-07-22 03:32:05 -04:00
|
|
|
from .rendering import RenderService
|
2026-07-22 01:29:32 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
2026-07-22 03:32:05 -04:00
|
|
|
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")
|
2026-07-22 01:29:32 -04:00
|
|
|
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)
|
2026-07-22 03:32:05 -04:00
|
|
|
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)
|
2026-07-22 01:29:32 -04:00
|
|
|
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())
|