From cf9be5b4b46179465fa0940a297838bf047dcf6e Mon Sep 17 00:00:00 2001 From: Ryan McLean Date: Sat, 17 Jan 2026 09:06:50 +0000 Subject: [PATCH 1/2] store raw manifest --- packages/constructor/README.md | 11 ++++ .../constructor/src/scp_constructor/cli.py | 62 +++++++++++++++++++ .../src/scp_constructor/neo4j_sync.py | 34 ++++++++++ 3 files changed, 107 insertions(+) diff --git a/packages/constructor/README.md b/packages/constructor/README.md index a5b7a97..0688754 100644 --- a/packages/constructor/README.md +++ b/packages/constructor/README.md @@ -63,6 +63,16 @@ export NEO4J_PASSWORD=password uv run scp-cli scan ./repos --export neo4j ``` +### Export from Neo4j + +```bash +# Export from Neo4j to JSON +uv run scp-cli export-neo4j --export json -o graph.json + +# Export from Neo4j to C4 diagram +uv run scp-cli export-neo4j --export c4 -o architecture.puml +``` + ## Commands | Command | Description | @@ -71,6 +81,7 @@ uv run scp-cli scan ./repos --export neo4j | `scp-cli scan ` | Scan local directory | | `scp-cli scan-github ` | Scan GitHub org | | `scp-cli transform ` | Transform JSON to other formats | +| `scp-cli export-neo4j` | Export from Neo4j to other formats | | `scp-cli version` | Show version | ## Export Formats diff --git a/packages/constructor/src/scp_constructor/cli.py b/packages/constructor/src/scp_constructor/cli.py index 058dc8a..34897ac 100644 --- a/packages/constructor/src/scp_constructor/cli.py +++ b/packages/constructor/src/scp_constructor/cli.py @@ -387,6 +387,67 @@ def transform( ) +@app.command("export-neo4j") +def export_neo4j( + export_format: str = typer.Option( + ..., "--export", "-e", help="Export format: json, mermaid, openc2, c4" + ), + output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file"), + stdout: bool = typer.Option( + False, "--stdout", help="Output to stdout instead of file" + ), + neo4j_uri: Optional[str] = typer.Option( + None, + "--neo4j-uri", + envvar="NEO4J_URI", + help="Neo4j URI (e.g., bolt://localhost:7687)", + ), + neo4j_user: Optional[str] = typer.Option( + None, "--neo4j-user", envvar="NEO4J_USER", help="Neo4j username" + ), + neo4j_password: Optional[str] = typer.Option( + None, "--neo4j-password", envvar="NEO4J_PASSWORD", help="Neo4j password" + ), +): + """Export architecture from Neo4j to other formats. + + Use this when you have previously synced SCP files to Neo4j and want to + export the graph to JSON, Mermaid, C4, or other formats without re-scanning. + """ + if not neo4j_uri: + console.print( + "[red]Error:[/] --neo4j-uri required (or set NEO4J_URI env var)" + ) + raise typer.Exit(1) + + user = neo4j_user or "neo4j" + password = neo4j_password or "neo4j" + + console.print(f"[bold blue]Connecting to Neo4j[/] {neo4j_uri}") + + try: + with Neo4jGraph(neo4j_uri, user, password) as graph: + manifests = graph.export_manifests() + console.print(f"Loaded [green]{len(manifests)}[/] systems from Neo4j") + + if not manifests: + console.print("[yellow]No systems found in graph[/]") + raise typer.Exit(0) + + # Create manifests list in expected format for _export_manifests + manifests_with_source = [(m, "neo4j-export") for m in manifests] + + _export_manifests( + manifests_with_source, + export_format, + output, + stdout, + ) + except Exception as e: + console.print(f"[red]Neo4j Error:[/] {e}") + raise typer.Exit(1) + + @app.command() def version(): """Show version information.""" @@ -395,3 +456,4 @@ def version(): if __name__ == "__main__": app() + diff --git a/packages/constructor/src/scp_constructor/neo4j_sync.py b/packages/constructor/src/scp_constructor/neo4j_sync.py index 8e6763f..6ccb139 100644 --- a/packages/constructor/src/scp_constructor/neo4j_sync.py +++ b/packages/constructor/src/scp_constructor/neo4j_sync.py @@ -1,5 +1,6 @@ """Neo4j graph builder for SCP architecture data.""" +import json from dataclasses import dataclass from neo4j import GraphDatabase @@ -94,6 +95,7 @@ def sync_manifest( s.domain = $domain, s.otel_service_name = $otel_service_name, s.source = $source, + s._raw_manifest = $raw_manifest, s.created_at = datetime(), s.updated_at = datetime() ON MATCH SET @@ -104,6 +106,7 @@ def sync_manifest( s.domain = $domain, s.otel_service_name = $otel_service_name, s.source = $source, + s._raw_manifest = $raw_manifest, s.updated_at = datetime() RETURN s.urn AS urn, CASE WHEN s.created_at = s.updated_at THEN 'created' ELSE 'updated' END AS action @@ -121,6 +124,7 @@ def sync_manifest( else None, "otel_service_name": manifest.otel_service_name, "source": source, + "raw_manifest": manifest.model_dump_json(), }, ) @@ -288,3 +292,33 @@ def get_blast_radius(self, system_urn: str, depth: int = 3) -> list[dict]: {"urn": system_urn, "depth": depth}, ) return [dict(record) for record in result] + + def export_manifests(self) -> list[SCPManifest]: + """Export all systems from Neo4j as SCPManifest objects. + + Reconstructs manifests from the stored _raw_manifest JSON field, + providing perfect fidelity with the original synced manifests. + + Returns: + List of reconstructed SCPManifest objects + """ + manifests: list[SCPManifest] = [] + + with self.driver.session(database=self.database) as session: + # Get all systems with raw manifest data + result = session.run(""" + MATCH (s:System) + WHERE s._raw_manifest IS NOT NULL + RETURN s._raw_manifest AS raw_manifest + ORDER BY s.tier, s.name + """) + + for record in result: + raw_json = record["raw_manifest"] + if raw_json: + manifest = SCPManifest.model_validate_json(raw_json) + manifests.append(manifest) + + return manifests + + From a1e4114683ef26ec6b272e01f34c681bfca3c74a Mon Sep 17 00:00:00 2001 From: Ryan McLean Date: Thu, 12 Feb 2026 11:09:24 +0000 Subject: [PATCH 2/2] remove unused import --- packages/constructor/src/scp_constructor/neo4j_sync.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/constructor/src/scp_constructor/neo4j_sync.py b/packages/constructor/src/scp_constructor/neo4j_sync.py index 6ccb139..203ea49 100644 --- a/packages/constructor/src/scp_constructor/neo4j_sync.py +++ b/packages/constructor/src/scp_constructor/neo4j_sync.py @@ -1,6 +1,5 @@ """Neo4j graph builder for SCP architecture data.""" -import json from dataclasses import dataclass from neo4j import GraphDatabase @@ -320,5 +319,3 @@ def export_manifests(self) -> list[SCPManifest]: manifests.append(manifest) return manifests - -