Skip to content

Commit c6e8595

Browse files
authored
Refactor Constructor and ServiceNow to use SDK Version 0.2.0 (#7)
* Refactor Constructor and ServiceNow to use SDK Version 0.2.0 * updated readmes
1 parent 102458b commit c6e8595

File tree

15 files changed

+274
-722
lines changed

15 files changed

+274
-722
lines changed

README.md

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,25 @@ Unified tooling for the **System Capability Protocol** (SCP). This monorepo cont
77
## Packages
88

99
### [constructor](./packages/constructor)
10+
1011
**Core tool for architecture definition and graph building**
1112

12-
Scan repositories for `scp.yaml` manifests, validate them, build dependency graphs, and export to various formats (JSON, Mermaid, Neo4j).
13+
Scan repositories for `scp.yaml` manifests, validate them, build dependency graphs, and export to various formats (JSON, Mermaid, Neo4j, OpenC2).
1314

1415
```bash
1516
uv run scp-cli scan ./repos --export mermaid
1617
```
1718

19+
### [servicenow](./packages/vendor/servicenow)
20+
21+
**ServiceNow CMDB integration**
22+
23+
Sync SCP architecture graphs to ServiceNow Configuration Management Database (CMDB).
24+
25+
```bash
26+
scp-servicenow cmdb sync graph.json
27+
```
28+
1829
## Quick Start
1930

2031
```bash
@@ -34,26 +45,14 @@ Run `make help` to see all available commands
3445

3546
## Architecture
3647

37-
```
38-
SCP Ecosystem
39-
40-
├── scp-definition (separate repo)
41-
│ └── Specification, schemas, examples
42-
43-
└── scp-integrations (this monorepo)
44-
45-
├── constructor (scan → validate → graph)
46-
│ ├── Local scanner
47-
│ ├── GitHub scanner
48-
│ ├── Validator
49-
│ └── Exporters (JSON, Mermaid, Neo4j)
50-
```
48+
**Built on [scp-sdk](https://github.com/krackenservices/scp-sdk) 0.2.0** - All packages use the SDK for manifest parsing, validation, and graph operations.
5149

5250
## What is SCP?
5351

54-
The **System Capability Protocol** provides a declarative manifest format (`scp.yaml`) that describes what a system *should* be, complementing OpenTelemetry's view of what *is happening*.
52+
The **System Capability Protocol** provides a declarative manifest format (`scp.yaml`) that describes what a system _should_ be, complementing OpenTelemetry's view of what _is happening_.
5553

5654
This enables:
55+
5756
- **LLM Reasoning**: Change impact analysis and migration planning
5857
- **Architecture Discovery**: Auto-generate org-wide system maps
5958
- **Theory vs Reality**: Diff declared dependencies against OTel traces
@@ -75,4 +74,3 @@ uv run pytest
7574
## License
7675

7776
MIT
78-

packages/constructor/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description = "Build architecture models from scp.yaml files"
55
readme = "README.md"
66
requires-python = ">=3.12"
77
dependencies = [
8-
"scp-sdk>=0.1.0",
8+
"scp-sdk>=0.2.0",
99
"typer>=0.9",
1010
"rich>=13.0",
1111
"neo4j>=5.0",

packages/constructor/src/scp_constructor/export.py

Lines changed: 11 additions & 194 deletions
Original file line numberDiff line numberDiff line change
@@ -2,106 +2,25 @@
22

33
from typing import Any
44

5-
from scp_sdk.core.models import SCPManifest
5+
from scp_sdk import (
6+
SCPManifest,
7+
export_graph_json,
8+
import_graph_json,
9+
)
610

711

812
def export_json(manifests: list[SCPManifest]) -> dict[str, Any]:
913
"""Export manifests to a JSON-serializable graph structure.
1014
15+
This is a wrapper around scp_sdk.export_graph_json() for backward compatibility.
16+
1117
Args:
1218
manifests: List of SCP manifests
1319
1420
Returns:
1521
Dictionary with nodes and edges lists
1622
"""
17-
nodes: list[dict] = []
18-
edges: list[dict] = []
19-
system_nodes: dict[str, dict] = {} # Track by URN for stub replacement
20-
21-
for manifest in manifests:
22-
urn = manifest.system.urn
23-
24-
# Add or update system node (replaces stub if exists)
25-
system_nodes[urn] = {
26-
"id": urn,
27-
"type": "System",
28-
"name": manifest.system.name,
29-
"tier": manifest.system.classification.tier
30-
if manifest.system.classification
31-
else None,
32-
"domain": manifest.system.classification.domain
33-
if manifest.system.classification
34-
else None,
35-
"team": manifest.ownership.team if manifest.ownership else None,
36-
"contacts": [
37-
{"type": c.type, "ref": c.ref} for c in manifest.ownership.contacts
38-
]
39-
if manifest.ownership and manifest.ownership.contacts
40-
else [],
41-
"escalation": manifest.ownership.escalation if manifest.ownership else [],
42-
}
43-
44-
# Add dependency edges (create stub only if not already known)
45-
if manifest.depends:
46-
for dep in manifest.depends:
47-
# Create stub node for dependency target if not seen
48-
if dep.system not in system_nodes:
49-
system_nodes[dep.system] = {
50-
"id": dep.system,
51-
"type": "System",
52-
"name": dep.system.split(":")[-1], # Extract name from URN
53-
"stub": True,
54-
}
55-
56-
edges.append(
57-
{
58-
"from": urn,
59-
"to": dep.system,
60-
"type": "DEPENDS_ON",
61-
"capability": dep.capability,
62-
"criticality": dep.criticality,
63-
"failure_mode": dep.failure_mode,
64-
}
65-
)
66-
67-
# Add capability nodes and PROVIDES edges
68-
if manifest.provides:
69-
for cap in manifest.provides:
70-
cap_id = f"{urn}:{cap.capability}"
71-
cap_node: dict[str, Any] = {
72-
"id": cap_id,
73-
"type": "Capability",
74-
"name": cap.capability,
75-
"capability_type": cap.type,
76-
}
77-
# Include security extension if present
78-
if cap.x_security:
79-
cap_node["x_security"] = {
80-
"actuator_profile": cap.x_security.actuator_profile,
81-
"actions": cap.x_security.actions,
82-
"targets": cap.x_security.targets,
83-
}
84-
nodes.append(cap_node)
85-
edges.append(
86-
{
87-
"from": urn,
88-
"to": cap_id,
89-
"type": "PROVIDES",
90-
}
91-
)
92-
93-
# Combine system nodes (from dict) with capability nodes (from list)
94-
all_nodes = list(system_nodes.values()) + nodes
95-
96-
return {
97-
"nodes": all_nodes,
98-
"edges": edges,
99-
"meta": {
100-
"systems_count": len(system_nodes),
101-
"capabilities_count": len(nodes),
102-
"dependencies_count": len([e for e in edges if e["type"] == "DEPENDS_ON"]),
103-
},
104-
}
23+
return export_graph_json(manifests)
10524

10625

10726
def export_mermaid(manifests: list[SCPManifest], direction: str = "LR") -> str:
@@ -342,6 +261,8 @@ def export_openc2(manifests: list[SCPManifest]) -> dict[str, Any]:
342261
def import_json(data: dict[str, Any]) -> list[SCPManifest]:
343262
"""Import manifests from a previously exported JSON graph.
344263
264+
This is a wrapper around scp_sdk.import_graph_json() for backward compatibility.
265+
345266
Reconstructs SCPManifest objects from the JSON export format,
346267
allowing transformation to other formats without re-scanning.
347268
@@ -351,108 +272,4 @@ def import_json(data: dict[str, Any]) -> list[SCPManifest]:
351272
Returns:
352273
List of reconstructed SCP manifests
353274
"""
354-
from scp_sdk.core.models import (
355-
System,
356-
Classification,
357-
Ownership,
358-
Capability,
359-
Dependency,
360-
SecurityExtension,
361-
)
362-
363-
manifests: list[SCPManifest] = []
364-
nodes = data.get("nodes", [])
365-
edges = data.get("edges", [])
366-
367-
# Build lookup maps
368-
system_nodes = {
369-
n["id"]: n for n in nodes if n.get("type") == "System" and not n.get("stub")
370-
}
371-
capability_nodes = {n["id"]: n for n in nodes if n.get("type") == "Capability"}
372-
373-
# Group edges by source system
374-
provides_by_system: dict[str, list[dict]] = {}
375-
depends_by_system: dict[str, list[dict]] = {}
376-
377-
for edge in edges:
378-
if edge["type"] == "PROVIDES":
379-
provides_by_system.setdefault(edge["from"], []).append(edge)
380-
elif edge["type"] == "DEPENDS_ON":
381-
depends_by_system.setdefault(edge["from"], []).append(edge)
382-
383-
# Reconstruct manifests
384-
for urn, node in system_nodes.items():
385-
# Build classification
386-
classification = None
387-
if node.get("tier") or node.get("domain"):
388-
classification = Classification(
389-
tier=node.get("tier"),
390-
domain=node.get("domain"),
391-
)
392-
393-
# Build ownership
394-
ownership = None
395-
if node.get("team"):
396-
from .models import Contact
397-
398-
contacts = []
399-
if node.get("contacts"):
400-
for c in node["contacts"]:
401-
contacts.append(Contact(type=c["type"], ref=c["ref"]))
402-
403-
ownership = Ownership(
404-
team=node["team"],
405-
contacts=contacts if contacts else None,
406-
escalation=node.get("escalation"),
407-
)
408-
409-
# Build capabilities
410-
provides = []
411-
for edge in provides_by_system.get(urn, []):
412-
cap_node = capability_nodes.get(edge["to"])
413-
if cap_node:
414-
# Check for security extension in capability node
415-
x_security = None
416-
if cap_node.get("x_security"):
417-
sec = cap_node["x_security"]
418-
x_security = SecurityExtension(
419-
actuator_profile=sec.get("actuator_profile"),
420-
actions=sec.get("actions", []),
421-
targets=sec.get("targets", []),
422-
)
423-
424-
provides.append(
425-
Capability(
426-
capability=cap_node["name"],
427-
type=cap_node.get("capability_type", "rest"),
428-
x_security=x_security,
429-
)
430-
)
431-
432-
# Build dependencies
433-
depends = []
434-
for edge in depends_by_system.get(urn, []):
435-
depends.append(
436-
Dependency(
437-
system=edge["to"],
438-
capability=edge.get("capability"),
439-
type="rest", # Default, as type isn't stored in edge
440-
criticality=edge.get("criticality", "required"),
441-
failure_mode=edge.get("failure_mode"),
442-
)
443-
)
444-
445-
manifest = SCPManifest(
446-
scp="0.1.0",
447-
system=System(
448-
urn=urn,
449-
name=node["name"],
450-
classification=classification,
451-
),
452-
ownership=ownership,
453-
provides=provides if provides else None,
454-
depends=depends if depends else None,
455-
)
456-
manifests.append(manifest)
457-
458-
return manifests
275+
return import_graph_json(data)

0 commit comments

Comments
 (0)