scripts.sync_ast_to_graph
Reference › scripts › sync_ast_to_graph
scripts/sync_ast_to_graph.py is the command-line wrapper for the AST sync engine: it loads an AST JSON file (produced by serialize_codebase_ast) into the knowledge graph as code nodes and relationships. A human runs it; for continuous sync see ast_live_sync. _make_syncer builds the GraphSyncer from the command-line flags.
Global flags are --uri (default KG_ROUTING_URI, then KG_URI), --user, --password (with no environment fallback, so omitting them means empty credentials), --database (default KG_DATABASE; needed for orchgraph, which serves orchkg and the per-worker orchkg_w<N> from one endpoint) and --dialect (neo4j or memgraph, default from KG_ENGINE; Memgraph has no IF NOT EXISTS forms and no full-text index, so the canonical statements are translated and the full-text ones dropped). Subcommands, dispatched by main to _cmd_* handlers: schema (create the schema), full (a full sync), map, incremental, stats and verify. An exception is printed as ERROR: and exits 1.
At a glance
| Source | scripts/sync_ast_to_graph.py |
| Lines of code | 361 |
| Classes | 0 |
| Functions | 9 |
| Imports (internal) | none |
| Imported by | none |
| Tests that import it | none |
Class diagram
No classes; see Functions.
Dependency diagram
No internal imports and no importers.
API
Functions
| Function | Signature | Summary |
|---|---|---|
_make_syncer |
_make_syncer(args: argparse.Namespace) -> GraphSyncer |
Instantiate GraphSyncer from CLI args (override env vars if supplied). |
_cmd_schema |
_cmd_schema(args: argparse.Namespace) -> int |
|
_cmd_full |
_cmd_full(args: argparse.Namespace) -> int |
|
_cmd_map |
_cmd_map(args: argparse.Namespace) -> int |
|
_cmd_incremental |
_cmd_incremental(args: argparse.Namespace) -> int |
|
_cmd_stats |
_cmd_stats(args: argparse.Namespace) -> int |
|
_cmd_verify |
_cmd_verify(args: argparse.Namespace) -> int |
|
_build_parser |
_build_parser() -> argparse.ArgumentParser |
Build the top-level argument parser with all subcommands. |
main |
main(argv: list[str] | None = None) -> None |
Entry point for the CLI wrapper. |
Source
#!/usr/bin/env python3
"""CLI wrapper for the Neo4j AST sync engine.
Reads a serialised AST JSON file produced by serialize_codebase_ast.py and
syncs the contents into a knowledge graph via GraphSyncer.
Subcommands
-----------
schema Create constraints and indexes (idempotent, safe to re-run).
full Full sync: clear existing project data and re-sync from AST JSON.
map Create cross-project MAPS_TO edges from class-mapping.json.
incremental Re-sync specific modules (delete + recreate + repair cross edges).
stats Print node/relationship counts per label/type.
verify Run verification queries and report pass/fail.
Connection URI defaults to the ``KG_ROUTING_URI`` environment variable
(falling back to ``KG_URI``); there is no username/password env fallback —
pass ``--user``/``--password`` explicitly, or ``--no-auth`` for the live
orchgraph cluster, which negotiates no authentication at all. CLI flags
override env vars. Global connection flags (``--uri``/``--user``/
``--password``/``--database``/``--dialect``/``--no-auth``) go BEFORE the
subcommand.
Examples
--------
python scripts/sync_ast_to_graph.py --uri "$KG_ROUTING_URI" \\
--database "$KG_DATABASE" --no-auth schema
python scripts/sync_ast_to_graph.py full \\
--ast plans/ast-ultimate.json --project orchestrata-ultimate
python scripts/sync_ast_to_graph.py map \\
--mapping plans/class-mapping.json
python scripts/sync_ast_to_graph.py incremental \\
--ast plans/ast-ultimate.json --project orchestrata-ultimate \\
--modules agents.base_agent,kernel.event_bus
python scripts/sync_ast_to_graph.py stats
python scripts/sync_ast_to_graph.py verify --project orchestrata-ultimate
"""
import argparse
# Allow running from the repo root without package installation.
# Use importlib to load sync.py directly, bypassing orchestrata_ultimate.__init__
# which has a broken import chain in core/traceability/__init__.py.
import importlib.util
import json
import sys
from pathlib import Path
_sync_path = (
Path(__file__).resolve().parent.parent
/ "src"
/ "orchestrata_ultimate"
/ "knowledge_graph"
/ "sync.py"
)
_spec = importlib.util.spec_from_file_location(
"orchestrata_ultimate.knowledge_graph.sync", _sync_path
)
if _spec is None or _spec.loader is None:
raise ImportError(f"Cannot load sync module from {_sync_path}")
_mod = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_mod)
GraphSyncer = _mod.GraphSyncer
def _make_syncer(args: argparse.Namespace) -> GraphSyncer:
"""Instantiate GraphSyncer from CLI args (override env vars if supplied)."""
# An explicitly-passed-but-EMPTY --uri must not silently fall back to the
# env default. `--uri "$KG_ROUTING_URI"` with that key blank expands to
# `--uri ""`, and the old `or None` turned that into "use the default
# graph" -- so `--uri "" --database orchkg schema` aimed orchgraph's DDL
# at the wrong server. Caught 2026-07-26 only because --no-auth made the
# wrong target reject the connection; without it the write would have landed.
uri_arg = getattr(args, "uri", None)
if uri_arg is not None and not uri_arg.strip():
raise SystemExit(
"--uri was passed but is empty; omit it to use $KG_ROUTING_URI "
"(falling back to $KG_URI), or pass a real address"
)
uri = uri_arg or None
user = getattr(args, "user", None) or None
password = getattr(args, "password", None) or None
database = getattr(args, "database", None) or None
no_auth = bool(getattr(args, "no_auth", False))
# No literal default: an unset --dialect must fall through to
# GraphSyncer -> resolve_kg_dialect(), which reads KG_ENGINE at CALL
# time. Hardcoding "neo4j" here overrode that resolver and sent Neo4j
# DDL ("CREATE TEXT INDEX ... IF NOT EXISTS") to an engine that had
# been orchgraph/Memgraph for hours, failing `schema` outright.
dialect = getattr(args, "dialect", None)
return GraphSyncer(
uri=uri,
username=user,
password=password,
database=database,
no_auth=no_auth,
dialect=dialect,
)
def _cmd_schema(args: argparse.Namespace) -> int:
syncer = _make_syncer(args)
syncer.connect()
try:
syncer.create_schema()
print("Schema created (constraints + indexes).")
return 0
finally:
syncer.close()
def _cmd_full(args: argparse.Namespace) -> int:
ast_path = Path(args.ast)
if not ast_path.exists():
print(f"ERROR: AST file not found: {ast_path}", file=sys.stderr)
return 1
with ast_path.open(encoding="utf-8") as fh:
ast_data = json.load(fh)
packages_meta: dict | None = None
if getattr(args, "packages_from", None):
mapping_path = Path(args.packages_from)
if mapping_path.exists():
with mapping_path.open(encoding="utf-8") as fh:
mapping = json.load(fh)
packages_meta = mapping.get("tier_assignments", {})
syncer = _make_syncer(args)
syncer.connect()
try:
syncer.full_sync(ast_data, args.project, packages_meta=packages_meta)
print(f"Full sync complete for project '{args.project}'.")
return 0
finally:
syncer.close()
def _cmd_map(args: argparse.Namespace) -> int:
mapping_path = Path(args.mapping)
if not mapping_path.exists():
print(f"ERROR: Mapping file not found: {mapping_path}", file=sys.stderr)
return 1
with mapping_path.open(encoding="utf-8") as fh:
mapping = json.load(fh)
source = getattr(args, "source_project", "orchestrata")
target = getattr(args, "target_project", "orchestrata-ultimate")
syncer = _make_syncer(args)
syncer.connect()
try:
syncer.map_classes(mapping, source, target)
print(f"MAPS_TO edges created from '{source}' -> '{target}'.")
return 0
finally:
syncer.close()
def _cmd_incremental(args: argparse.Namespace) -> int:
ast_path = Path(args.ast)
if not ast_path.exists():
print(f"ERROR: AST file not found: {ast_path}", file=sys.stderr)
return 1
with ast_path.open(encoding="utf-8") as fh:
ast_data = json.load(fh)
modules = [m.strip() for m in args.modules.split(",") if m.strip()]
if not modules:
print(
"ERROR: --modules must be a comma-separated list of module paths.",
file=sys.stderr,
)
return 1
syncer = _make_syncer(args)
syncer.connect()
try:
syncer.incremental_sync(ast_data, args.project, modules)
print(
f"Incremental sync complete for {len(modules)} module(s) "
f"in project '{args.project}'."
)
return 0
finally:
syncer.close()
def _cmd_stats(args: argparse.Namespace) -> int:
syncer = _make_syncer(args)
syncer.connect()
try:
stats = syncer.get_stats()
print("\n=== Node Counts ===")
for label, count in stats["nodes"].items():
print(f" {label:<14} {count:>8}")
print("\n=== Relationship Counts ===")
for rel_type, count in stats["relationships"].items():
print(f" {rel_type:<20} {count:>8}")
total_nodes = sum(stats["nodes"].values())
total_rels = sum(stats["relationships"].values())
print(f"\nTotal nodes: {total_nodes} | Total relationships: {total_rels}")
return 0
finally:
syncer.close()
def _cmd_verify(args: argparse.Namespace) -> int:
syncer = _make_syncer(args)
syncer.connect()
try:
result = syncer.verify(args.project)
print(f"\n=== Verification: {args.project} ===")
ok = result.pop("ok", False)
for key, value in result.items():
status = "PASS" if value >= 1 else "WARN"
print(f" {status} {key:<20} = {value}")
if ok:
print("\nResult: PASS")
return 0
else:
print("\nResult: FAIL — hierarchy incomplete")
return 2
finally:
syncer.close()
def _build_parser() -> argparse.ArgumentParser:
"""Build the top-level argument parser with all subcommands."""
parser = argparse.ArgumentParser(
prog="sync_ast_to_graph",
description="Sync an AST JSON file into a knowledge graph.",
)
# Global connection flags
parser.add_argument(
"--uri",
default=None,
help="Graph Bolt URI (default: KG_ROUTING_URI, falling back to KG_URI).",
)
parser.add_argument(
"--user",
default=None,
help="Graph username. No env fallback: omitted means empty credentials.",
)
parser.add_argument(
"--password",
default=None,
help="Graph password. No env fallback: omitted means empty credentials.",
)
parser.add_argument(
"--database",
default=None,
help="Target database (default: KG_DATABASE, else the server's "
"default). Needed for orchgraph, which serves orchkg and the "
"per-worker orchkg_w<N> from one endpoint.",
)
parser.add_argument(
"--dialect",
default=None,
choices=["neo4j", "memgraph"],
help="DDL dialect for the `schema` subcommand (default: resolved "
"from KG_ENGINE at call time). Memgraph has no "
"IF NOT EXISTS on any index/constraint form and no FULLTEXT index at "
"all, so the canonical Neo4j statements are translated (and the "
"fulltext ones dropped) rather than sent verbatim.",
)
parser.add_argument(
"--no-auth",
action="store_true",
help="Connect with NO authentication. Required for orchgraph, which "
"negotiates none; without it the syncer presents whatever "
"--user/--password supplied (empty strings by default) to a server "
"that wants no credentials at all.",
)
sub = parser.add_subparsers(dest="command", required=True)
# schema
sub.add_parser("schema", help="Create Neo4j constraints and indexes.")
# full
full_p = sub.add_parser("full", help="Full sync: clear and re-sync a project.")
full_p.add_argument("--ast", required=True, help="Path to the AST JSON file.")
full_p.add_argument("--project", required=True, help="Project name (graph root).")
full_p.add_argument(
"--packages-from",
default=None,
help="Path to class-mapping.json for package tier/loc metadata.",
)
# map
map_p = sub.add_parser("map", help="Create MAPS_TO edges from class-mapping.json.")
map_p.add_argument("--mapping", required=True, help="Path to class-mapping.json.")
map_p.add_argument(
"--source-project",
default="orchestrata",
help="Source project name (default: orchestrata).",
)
map_p.add_argument(
"--target-project",
default="orchestrata-ultimate",
help="Target project name (default: orchestrata-ultimate).",
)
# incremental
inc_p = sub.add_parser(
"incremental", help="Incrementally re-sync specific modules."
)
inc_p.add_argument("--ast", required=True, help="Path to the AST JSON file.")
inc_p.add_argument("--project", required=True, help="Project name.")
inc_p.add_argument(
"--modules",
required=True,
help="Comma-separated list of module paths to re-sync.",
)
# stats
sub.add_parser("stats", help="Print node/relationship counts.")
# verify
verify_p = sub.add_parser(
"verify", help="Run verification queries against a synced project."
)
verify_p.add_argument("--project", required=True, help="Project name to verify.")
return parser
def main(argv: list[str] | None = None) -> None:
"""Entry point for the CLI wrapper."""
parser = _build_parser()
args = parser.parse_args(argv)
dispatch = {
"schema": _cmd_schema,
"full": _cmd_full,
"map": _cmd_map,
"incremental": _cmd_incremental,
"stats": _cmd_stats,
"verify": _cmd_verify,
}
handler = dispatch.get(args.command)
if handler is None:
parser.print_help()
sys.exit(1)
try:
exit_code = handler(args)
except Exception as exc: # noqa: BLE001
print(f"ERROR: {exc}", file=sys.stderr)
sys.exit(1)
sys.exit(exit_code)
if __name__ == "__main__":
main()