scripts.bootstrap_graphs
Reference › scripts › bootstrap_graphs
An idempotent bootstrap for both graphs, the knowledge graph and Memgraph. It derives graph state from source, never by hand. A human or a provisioning step runs it; no module or test imports it.
Invocation: main requires at least one of --kg, --memgraph, --all, --check or --plan, and exits 2 otherwise. --neo4j is a deprecated alias of --kg and prints a warning. --plan prints the steps without running anything (_print_plan), and --check verifies the current state (_check_kg, _check_memgraph) and reports what is missing. _resolve_targets decides which graphs are in play.
Each graph has an ordered chain of Step records (_kg_steps, _memgraph_steps), and _run_steps executes them, aborting when a required step fails and skipping optional ones. The environment is the process environment merged with a dotenv file (_effective_env). The step lists can be passed as keyword arguments to main so tests can exercise the dispatch without touching shared infrastructure.
At a glance
| Source | scripts/bootstrap_graphs.py |
| Lines of code | 439 |
| Classes | 1 |
| Functions | 10 |
| Imports (internal) | none |
| Imported by | none |
| Tests that import it | none |
Class diagram
Dependency diagram
No internal imports and no importers.
API
Classes
Step (dataclass)
Decorators: dataclass(frozen=True) · Line 84
One orchestrated building-block invocation.
| Attribute | Type | Default |
|---|---|---|
name |
str |
|
argv |
list[str] |
|
optional |
bool |
False |
requires |
Path | None |
field(default=None) |
Functions
| Function | Signature | Summary |
|---|---|---|
_effective_env |
_effective_env(env: Mapping[str, str] | None, dotenv_path: Path | None = None) -> dict[str, str] |
Build the env mapping passed to every subprocess step. |
_kg_steps |
_kg_steps() -> list[Step] |
Build the ordered operational-KG bootstrap chain (SPEC-106 building blocks). |
_memgraph_steps |
_memgraph_steps() -> list[Step] |
Build the ordered Memgraph bootstrap chain. |
_run_steps |
_run_steps(steps: list[Step], env: Mapping[str, str]) -> int |
Execute steps in order against env; abort on the first hard failure. |
_print_plan |
_print_plan(steps: list[Step]) -> None |
Print the ordered step list without executing anything (the runbook). |
_check_kg |
_check_kg(env: Mapping[str, str]) -> tuple[bool, list[str]] |
Report operational-KG schema drift via kg_schema.py check (read-only). |
_check_memgraph |
_check_memgraph(env: Mapping[str, str]) -> tuple[bool, list[str]] |
Light Memgraph reachability + project-scoped-node check (read-only). |
_resolve_targets |
_resolve_targets(args: argparse.Namespace) -> tuple[bool, bool] |
Resolve which graph(s) a flag combination targets. |
_build_parser |
_build_parser() -> argparse.ArgumentParser |
|
main |
main(argv: list[str] | None = None, env: Mapping[str, str] | None = None, *, kg_steps: list[Step] | None = None, memgraph_steps: list[Step] | None = None) -> int |
CLI entry point. |
Source
#!/usr/bin/env python3
"""Idempotent bootstrap for both graphs — derive graph state from source, never hand-build.
Orchestrating CLI for plans/reproducibility-plan.md Phase R4. It does NOT
reimplement any graph-writing logic itself — it composes the EXISTING
building-block scripts as subprocess steps, so both graphs can be rebuilt
from source + seeds through ONE entry point:
KG (operational knowledge graph) chain:
1. ``kg_schema.py apply`` — canonical constraints/indexes (SPEC-106).
2. ``serialize_codebase_ast.py serialize`` — regenerate the AST JSON, ONLY if
``plans/ast-ultimate.json`` is missing (best-effort; a failure here just
skips the code-intelligence step below with a warning, it never aborts).
3. ``sync_ast_to_graph.py schema`` + ``... full --ast <path>`` — code
intelligence (CodeFunction/CodeClass/Method); skipped with a logged
warning if no AST JSON is available.
4. ``sync_architecture_to_graph.py --repo-adrs docs/adr`` — architecture/
component/rule nodes plus one ``ArchitectureDecision`` per
``docs/adr/*.md`` (D5).
5. ``enrich_semantic_graph_specs.py`` — specs/RTM (spec side).
Memgraph (semantic/RAG) chain:
1. ``ingest_orchestrata_ultimate.py`` — source -> chunk/embed -> Memgraph.
Respects shared-cluster discipline (label-scoped, project_id-tagged;
never touches ``:Entity`` nodes this project didn't create).
Modes
-----
``--kg`` / ``--memgraph`` / ``--all``
Scope which graph(s) to target. Combine with ``--check`` or ``--plan``.
With neither ``--check`` nor ``--plan``, actually executes the chain(s)
(mutates the live graph(s)) — each step is logged before it runs, and any
non-optional step failure aborts with a clear message and non-zero exit.
``--neo4j`` is accepted as a deprecated alias for ``--kg`` (the operational
KG no longer runs on Neo4j at all — same orchgraph/Memgraph cluster as the
semantic graph, different database — the flag is graph-role-named now).
``--check``
READ-ONLY. Reports drift instead of mutating: ``kg_schema.py check``
(live KG schema vs. ``canonical_schema.py``) and/or a light Memgraph
reachability + project-scoped-node check. Exits 0 iff no drift was found.
With no scope flag, checks BOTH graphs (same as ``--all --check``).
``--plan``
READ-ONLY. Prints the ordered step list (name + exact argv) that the
matching ``--kg``/``--memgraph``/``--all`` run would execute, without
running or connecting to anything. Also the runbook: paste the printed
commands to run steps by hand.
Examples
--------
python scripts/bootstrap_graphs.py --check
python scripts/bootstrap_graphs.py --all --check
python scripts/bootstrap_graphs.py --all --plan
python scripts/bootstrap_graphs.py --kg
python scripts/bootstrap_graphs.py --memgraph
"""
from __future__ import annotations
import argparse
import logging
import os
import subprocess
import sys
from collections.abc import Mapping
from dataclasses import dataclass, field
from pathlib import Path
from dotenv import dotenv_values
from neo4j import GraphDatabase
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
)
logger = logging.getLogger(__name__)
REPO_ROOT = Path(__file__).resolve().parent.parent
SCRIPTS_DIR = REPO_ROOT / "scripts"
AST_JSON_PATH = REPO_ROOT / "plans" / "ast-ultimate.json"
PROJECT_ID = "orchestrata-ultimate"
@dataclass(frozen=True)
class Step:
"""One orchestrated building-block invocation."""
name: str
argv: list[str]
optional: bool = False
"""If True, a non-zero exit logs a warning and the run continues."""
requires: Path | None = field(default=None)
"""If set and missing at run time, the step is skipped with a warning
(not counted as a failure) instead of being executed."""
def _effective_env(
env: Mapping[str, str] | None,
dotenv_path: Path | None = None,
) -> dict[str, str]:
"""Build the env mapping passed to every subprocess step.
Starts from ``env`` (the injectable seam — defaults to a copy of the
real ``os.environ``) and fills in any keys missing from it using the
project ``.env`` file, mirroring the ``os.environ.get(key, default)``
convention: explicit/injected values always win over the file default.
Never mutates the real ``os.environ``.
Args:
env: Base environment mapping. Defaults to a copy of ``os.environ``.
dotenv_path: Optional path to a ``.env`` file. Defaults to the
project root ``.env``.
"""
base: dict[str, str] = dict(env) if env is not None else dict(os.environ)
dotenv_path = dotenv_path if dotenv_path is not None else REPO_ROOT / ".env"
if dotenv_path.exists():
for key, value in dotenv_values(dotenv_path).items():
if value is not None and key not in base:
base[key] = value
return base
def _kg_steps() -> list[Step]:
"""Build the ordered operational-KG bootstrap chain (SPEC-106 building blocks)."""
py = sys.executable
steps: list[Step] = [
Step(
"kg_schema.apply",
[py, str(SCRIPTS_DIR / "kg_schema.py"), "apply"],
)
]
if not AST_JSON_PATH.exists():
steps.append(
Step(
"serialize_codebase_ast.generate",
[
py,
str(SCRIPTS_DIR / "serialize_codebase_ast.py"),
"serialize",
"--source",
"src/orchestrata_ultimate",
"--name",
PROJECT_ID,
"--output",
str(AST_JSON_PATH),
],
optional=True,
)
)
steps.append(
Step(
"sync_ast_to_graph.schema",
[py, str(SCRIPTS_DIR / "sync_ast_to_graph.py"), "schema"],
requires=AST_JSON_PATH,
)
)
steps.append(
Step(
"sync_ast_to_graph.full",
[
py,
str(SCRIPTS_DIR / "sync_ast_to_graph.py"),
"full",
"--ast",
str(AST_JSON_PATH),
"--project",
PROJECT_ID,
],
requires=AST_JSON_PATH,
)
)
steps.append(
Step(
"sync_architecture_to_graph",
[
py,
str(SCRIPTS_DIR / "sync_architecture_to_graph.py"),
"--repo-adrs",
str(REPO_ROOT / "docs" / "adr"),
],
)
)
steps.append(
Step(
"enrich_semantic_graph_specs",
[py, str(SCRIPTS_DIR / "enrich_semantic_graph_specs.py")],
)
)
return steps
def _memgraph_steps() -> list[Step]:
"""Build the ordered Memgraph bootstrap chain."""
py = sys.executable
return [
Step(
"ingest_orchestrata_ultimate",
[py, str(SCRIPTS_DIR / "ingest_orchestrata_ultimate.py")],
)
]
def _run_steps(steps: list[Step], env: Mapping[str, str]) -> int:
"""Execute ``steps`` in order against ``env``; abort on the first hard failure."""
for step in steps:
if step.requires is not None and not step.requires.exists():
logger.warning(
"Skipping step %s — required input not found: %s",
step.name,
step.requires,
)
continue
logger.info("Running step: %s", step.name)
logger.info(" $ %s", " ".join(step.argv))
result = subprocess.run(step.argv, cwd=REPO_ROOT, env=dict(env))
if result.returncode != 0:
if step.optional:
logger.warning(
"Optional step %s failed (exit %s) — continuing",
step.name,
result.returncode,
)
continue
logger.error(
"Step %s failed (exit %s) — aborting bootstrap",
step.name,
result.returncode,
)
return result.returncode
return 0
def _print_plan(steps: list[Step]) -> None:
"""Print the ordered step list without executing anything (the runbook)."""
print(f"Planned {len(steps)} step(s):")
for idx, step in enumerate(steps, start=1):
tags = []
if step.optional:
tags.append("optional")
if step.requires is not None:
tags.append(f"requires={step.requires}")
suffix = f" [{', '.join(tags)}]" if tags else ""
print(f" {idx}. {step.name}{suffix}")
print(f" $ {' '.join(step.argv)}")
def _check_kg(env: Mapping[str, str]) -> tuple[bool, list[str]]:
"""Report operational-KG schema drift via ``kg_schema.py check`` (read-only)."""
py = sys.executable
result = subprocess.run(
[py, str(SCRIPTS_DIR / "kg_schema.py"), "check"],
cwd=REPO_ROOT,
env=dict(env),
capture_output=True,
text=True,
)
lines = [
line for line in (result.stdout + result.stderr).splitlines() if line.strip()
]
if not lines:
lines = [f"kg_schema.py check exited {result.returncode} with no output."]
return result.returncode == 0, lines
def _check_memgraph(env: Mapping[str, str]) -> tuple[bool, list[str]]:
"""Light Memgraph reachability + project-scoped-node check (read-only).
Never queries by label (the cluster is shared with knowledge-wiki) —
only by the ``project_id`` property every node this project writes
carries (see ``semantic_graph/ingestion.py``), so this can never touch
or even see another project's ``:Entity``/``:Chunk`` nodes.
"""
uri = env.get("MEMGRAPH_URI")
if not uri:
raise RuntimeError(
"No graph URI configured — set MEMGRAPH_URI (see .env); this "
"code has no default address (RUL-003). It used to fall back to "
"bolt://localhost:7687, which nothing in this infrastructure "
"listens on."
)
user = env.get("MEMGRAPH_USERNAME") or None
password = env.get("MEMGRAPH_PASSWORD", "")
auth = (user, password) if user else None
driver = GraphDatabase.driver(uri, auth=auth)
try:
driver.verify_connectivity()
except Exception as exc: # noqa: BLE001
driver.close()
return False, [f"Memgraph unreachable at {uri}: {exc}"]
try:
with driver.session() as session:
record = session.run(
"MATCH (n) WHERE n.project_id = $pid RETURN count(n) AS c",
{"pid": PROJECT_ID},
).single()
count = record["c"] if record else 0
finally:
driver.close()
if count == 0:
return False, [
f"No {PROJECT_ID}-tagged nodes found in Memgraph ({uri}) — "
"semantic graph has not been ingested from source."
]
return True, [
f"OK — {count} {PROJECT_ID}-tagged node(s) found in Memgraph ({uri})."
]
def _resolve_targets(args: argparse.Namespace) -> tuple[bool, bool]:
"""Resolve which graph(s) a flag combination targets.
``--all`` always means both. A bare scope flag (``--kg``/``--memgraph``)
scopes to just that graph. With no scope flag at all (bare ``--check`` or
bare ``--plan``), both graphs are targeted.
"""
if args.all:
return True, True
if args.kg or args.memgraph:
return args.kg, args.memgraph
return True, True
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="bootstrap_graphs",
description=(
"Idempotent bootstrap for both graphs — derive graph state from "
"source, never hand-build (reproducibility-plan.md Phase R4)."
),
)
parser.add_argument(
"--kg", action="store_true", help="Target the operational knowledge graph (KG)."
)
parser.add_argument(
"--neo4j",
action="store_true",
dest="neo4j_legacy",
help=argparse.SUPPRESS, # deprecated alias for --kg (graph-rename wave)
)
parser.add_argument(
"--memgraph",
action="store_true",
help="Target the Memgraph semantic/RAG graph.",
)
parser.add_argument("--all", action="store_true", help="Target both graphs.")
parser.add_argument(
"--check",
action="store_true",
help="Read-only: report schema/ingest drift instead of mutating.",
)
parser.add_argument(
"--plan",
action="store_true",
help="Read-only: print the ordered step list without executing.",
)
return parser
def main(
argv: list[str] | None = None,
env: Mapping[str, str] | None = None,
*,
kg_steps: list[Step] | None = None,
memgraph_steps: list[Step] | None = None,
) -> int:
"""CLI entry point.
``kg_steps``/``memgraph_steps`` default to the real building-block
chains (:func:`_kg_steps`/:func:`_memgraph_steps`) — every real CLI
invocation runs them. The keyword-only overrides exist purely so tests
can exercise the full ``--plan``/``--check``/execute dispatch (including
the abort-on-failure and optional-skip control flow in
:func:`_run_steps`) end-to-end without ever running the real chains
against shared live infra — mirroring the same pattern used by
``kg_schema.py``'s ``_cmd_apply(..., *, statements=None)``.
"""
parser = _build_parser()
args = parser.parse_args(argv)
if args.neo4j_legacy:
print(
"WARNING: --neo4j is deprecated, use --kg instead.",
file=sys.stderr,
)
args.kg = True
if not (args.kg or args.memgraph or args.all or args.check or args.plan):
parser.print_usage(sys.stderr)
print(
"ERROR: specify at least one of --kg/--memgraph/--all/--check/--plan",
file=sys.stderr,
)
return 2
do_kg, do_memgraph = _resolve_targets(args)
effective_env = _effective_env(env)
resolved_kg_steps = kg_steps if kg_steps is not None else _kg_steps()
resolved_memgraph_steps = (
memgraph_steps if memgraph_steps is not None else _memgraph_steps()
)
if args.plan:
steps: list[Step] = []
if do_kg:
steps.extend(resolved_kg_steps)
if do_memgraph:
steps.extend(resolved_memgraph_steps)
_print_plan(steps)
return 0
if args.check:
ok = True
if do_kg:
print("=== KG schema check (kg_schema.py check) ===")
kg_ok, lines = _check_kg(effective_env)
for line in lines:
print(line)
ok = ok and kg_ok
if do_memgraph:
print("=== Memgraph reachability/label check ===")
mg_ok, lines = _check_memgraph(effective_env)
for line in lines:
print(line)
ok = ok and mg_ok
return 0 if ok else 1
exit_code = 0
if do_kg:
logger.info("=== KG bootstrap chain ===")
exit_code = _run_steps(resolved_kg_steps, effective_env)
if exit_code != 0:
return exit_code
if do_memgraph:
logger.info("=== Memgraph bootstrap ===")
exit_code = _run_steps(resolved_memgraph_steps, effective_env)
return exit_code
if __name__ == "__main__":
sys.exit(main())