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
|
2026-07-25 16:00:19 -04:00
|
|
|
import webbrowser
|
2026-07-22 01:29:32 -04:00
|
|
|
from pathlib import Path
|
|
|
|
|
|
2026-07-25 16:00:19 -04:00
|
|
|
from .application import CanonicalApplicationService, GenericCanonicalApplier
|
2026-07-22 01:29:32 -04:00
|
|
|
from .context import compile_context
|
|
|
|
|
from .errors import DocForgeError
|
|
|
|
|
from .index import ProjectIndex
|
2026-07-27 15:50:33 -04:00
|
|
|
from .onboarding import assess_project, scaffold_project
|
2026-07-22 01:29:32 -04:00
|
|
|
from .project import Project, project_root_fingerprint
|
2026-07-22 03:32:05 -04:00
|
|
|
from .rendering import RenderService
|
2026-07-29 05:07:16 -04:00
|
|
|
from .telemetry import request
|
2026-07-25 16:00:19 -04:00
|
|
|
from .viewer_manager import ViewerManagerClient
|
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)
|
2026-07-29 05:07:16 -04:00
|
|
|
parser.add_argument(
|
|
|
|
|
"--diagnostics",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Attach bounded request-local stage timings and counters",
|
|
|
|
|
)
|
2026-07-22 01:29:32 -04:00
|
|
|
commands = parser.add_subparsers(dest="command", required=True)
|
2026-07-27 15:50:33 -04:00
|
|
|
onboard = commands.add_parser("onboard")
|
|
|
|
|
onboard.add_argument("--language", action="append", default=[])
|
|
|
|
|
onboard.add_argument("--scaffold", action="store_true")
|
|
|
|
|
onboard.add_argument("--project-id")
|
|
|
|
|
onboard.add_argument("--title")
|
|
|
|
|
onboard.add_argument("--content-root", default="docs/docforge/content")
|
2026-07-22 01:29:32 -04:00
|
|
|
commands.add_parser("info")
|
|
|
|
|
commands.add_parser("validate")
|
|
|
|
|
commands.add_parser("build")
|
2026-07-25 16:00:19 -04:00
|
|
|
commands.add_parser("reindex")
|
2026-07-26 09:32:25 -04:00
|
|
|
commands.add_parser("sync")
|
2026-07-22 01:29:32 -04:00
|
|
|
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)
|
2026-07-29 04:09:28 -04:00
|
|
|
command.add_argument("--limit", type=int)
|
2026-07-22 01:29:32 -04:00
|
|
|
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="?")
|
2026-07-29 04:42:55 -04:00
|
|
|
render_status.add_argument("--deep", action="store_true")
|
2026-07-22 03:32:05 -04:00
|
|
|
preview = commands.add_parser("preview")
|
|
|
|
|
preview.add_argument("changeset_id")
|
|
|
|
|
preview.add_argument("view_id")
|
2026-07-25 16:00:19 -04:00
|
|
|
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")
|
2026-07-22 01:29:32 -04:00
|
|
|
return parser
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _run(arguments: argparse.Namespace) -> dict[str, object]:
|
2026-07-27 15:50:33 -04:00
|
|
|
if arguments.command == "onboard":
|
|
|
|
|
languages = tuple(arguments.language)
|
|
|
|
|
if arguments.scaffold:
|
|
|
|
|
scaffold = scaffold_project(
|
|
|
|
|
arguments.project_root,
|
|
|
|
|
requested_languages=languages,
|
|
|
|
|
project_id=arguments.project_id,
|
|
|
|
|
title=arguments.title,
|
|
|
|
|
content_root=arguments.content_root,
|
|
|
|
|
)
|
|
|
|
|
project = Project.open(arguments.project_root)
|
|
|
|
|
build = ProjectIndex(project).build()
|
|
|
|
|
render = RenderService(project).render("manual")
|
|
|
|
|
return {**scaffold, "build": build, "render": render}
|
|
|
|
|
return assess_project(arguments.project_root, requested_languages=languages)
|
2026-07-22 01:29:32 -04:00
|
|
|
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()
|
2026-07-25 16:00:19 -04:00
|
|
|
if arguments.command == "reindex":
|
|
|
|
|
built = index.build()
|
|
|
|
|
return {
|
|
|
|
|
**built,
|
|
|
|
|
"reindexed": True,
|
|
|
|
|
"check": index.check(),
|
|
|
|
|
}
|
2026-07-26 09:32:25 -04:00
|
|
|
if arguments.command == "sync":
|
|
|
|
|
return index.synchronize()
|
2026-07-22 01:29:32 -04:00
|
|
|
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":
|
2026-07-29 04:09:28 -04:00
|
|
|
return index.backlinks(
|
|
|
|
|
arguments.node_id,
|
|
|
|
|
relation=arguments.relation,
|
|
|
|
|
limit=arguments.limit,
|
|
|
|
|
)
|
2026-07-22 01:29:32 -04:00
|
|
|
if arguments.command == "dependencies":
|
2026-07-29 04:09:28 -04:00
|
|
|
return index.dependencies(
|
|
|
|
|
arguments.node_id,
|
|
|
|
|
depth=arguments.depth,
|
|
|
|
|
limit=arguments.limit,
|
|
|
|
|
)
|
2026-07-22 01:29:32 -04:00
|
|
|
if arguments.command == "impact":
|
2026-07-29 04:09:28 -04:00
|
|
|
return index.impact(
|
|
|
|
|
arguments.node_id,
|
|
|
|
|
depth=arguments.depth,
|
|
|
|
|
limit=arguments.limit,
|
|
|
|
|
)
|
2026-07-22 01:29:32 -04:00
|
|
|
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":
|
2026-07-29 04:42:55 -04:00
|
|
|
rendering = RenderService(project)
|
|
|
|
|
return (
|
|
|
|
|
rendering.deep_status(arguments.view_id)
|
|
|
|
|
if arguments.deep
|
|
|
|
|
else rendering.status(arguments.view_id)
|
|
|
|
|
)
|
2026-07-22 03:32:05 -04:00
|
|
|
if arguments.command == "preview":
|
|
|
|
|
return RenderService(project).preview(arguments.changeset_id, arguments.view_id)
|
2026-07-25 16:00:19 -04:00
|
|
|
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()
|
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)
|
2026-07-29 05:07:16 -04:00
|
|
|
with request(
|
|
|
|
|
f"cli.{arguments.command}",
|
|
|
|
|
enabled=arguments.diagnostics,
|
|
|
|
|
) as collector:
|
|
|
|
|
try:
|
|
|
|
|
result = _run(arguments)
|
|
|
|
|
code = 0
|
|
|
|
|
except DocForgeError as error:
|
|
|
|
|
result = {"status": "error", "error": error.as_dict()}
|
|
|
|
|
code = 2
|
|
|
|
|
if collector is not None:
|
|
|
|
|
result["diagnostics"] = collector.as_dict(
|
|
|
|
|
outcome="ok" if code == 0 else "error",
|
|
|
|
|
)
|
2026-07-22 01:29:32 -04:00
|
|
|
print(json.dumps(result, sort_keys=True, indent=2))
|
|
|
|
|
return code
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
sys.exit(main())
|