domain-context-graph 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dcg/__init__.py +41 -0
- dcg/adapters/__init__.py +8 -0
- dcg/adapters/base.py +156 -0
- dcg/adapters/graphify.py +233 -0
- dcg/adapters/registry.py +18 -0
- dcg/cli.py +206 -0
- dcg/core/__init__.py +63 -0
- dcg/core/builders.py +68 -0
- dcg/core/model.py +113 -0
- dcg/core/ontology.py +386 -0
- dcg/core/packs/code.yaml +27 -0
- dcg/core/packs/core.yaml +65 -0
- dcg/core/packs/dcg-development.yaml +22 -0
- dcg/core/packs/security.yaml +9 -0
- dcg/core/project.py +247 -0
- dcg/core/purge.py +9 -0
- dcg/core/stack.py +643 -0
- dcg/core/store.py +408 -0
- dcg/core/uid.py +28 -0
- dcg/ingest/__init__.py +58 -0
- dcg/ingest/discovery.py +69 -0
- dcg/ingest/models.py +132 -0
- dcg/ingest/report.py +47 -0
- dcg/ingest/runner.py +295 -0
- dcg/mcp/__init__.py +6 -0
- dcg/mcp/__main__.py +4 -0
- dcg/mcp/dcg_ingest_mcp.py +89 -0
- dcg/mcp/dcg_mcp.py +79 -0
- dcg/mcp/helpers.py +12 -0
- dcg/mcp/state.py +20 -0
- dcg/mcp/tools/__init__.py +1 -0
- dcg/mcp/tools/ingest.py +161 -0
- dcg/mcp/tools/project.py +37 -0
- dcg/mcp/tools/query.py +145 -0
- dcg/mcp/tools/stack.py +99 -0
- dcg/ops/__init__.py +1 -0
- dcg/ops/ingest.py +167 -0
- dcg/ops/lifecycle.py +86 -0
- dcg/ops/ontology.py +37 -0
- dcg/ui/__init__.py +116 -0
- dcg/ui/index.html +12 -0
- dcg/ui/package-lock.json +5172 -0
- dcg/ui/package.json +41 -0
- dcg/ui/src/App.tsx +35 -0
- dcg/ui/src/__tests__/store.test.ts +65 -0
- dcg/ui/src/components/AppShell.tsx +45 -0
- dcg/ui/src/components/AttributeRow.tsx +16 -0
- dcg/ui/src/components/CreateEntityDialog.tsx +82 -0
- dcg/ui/src/components/DetailPanel.tsx +20 -0
- dcg/ui/src/components/DomainFilter.tsx +65 -0
- dcg/ui/src/components/EntityDetail.tsx +229 -0
- dcg/ui/src/components/EntityForm.tsx +117 -0
- dcg/ui/src/components/GraphCanvas.tsx +119 -0
- dcg/ui/src/components/LayerDetail.tsx +53 -0
- dcg/ui/src/components/LayerSwitcher.tsx +34 -0
- dcg/ui/src/components/RelationDetail.tsx +55 -0
- dcg/ui/src/components/RelationRow.tsx +43 -0
- dcg/ui/src/components/SearchInput.tsx +26 -0
- dcg/ui/src/components/Sidebar.tsx +13 -0
- dcg/ui/src/components/StackTopology.tsx +147 -0
- dcg/ui/src/components/StatusBar.tsx +24 -0
- dcg/ui/src/components/Toolbar.tsx +88 -0
- dcg/ui/src/components/TypeFilter.tsx +36 -0
- dcg/ui/src/components/ViewModeToggle.tsx +30 -0
- dcg/ui/src/declarations.d.ts +5 -0
- dcg/ui/src/graph/__tests__/transform.test.ts +114 -0
- dcg/ui/src/graph/layouts.ts +36 -0
- dcg/ui/src/graph/styles.ts +90 -0
- dcg/ui/src/graph/transform.ts +140 -0
- dcg/ui/src/index.css +1 -0
- dcg/ui/src/main.tsx +10 -0
- dcg/ui/src/mcp/McpProvider.tsx +57 -0
- dcg/ui/src/mcp/StaticProvider.tsx +24 -0
- dcg/ui/src/mcp/__tests__/types.test.ts +45 -0
- dcg/ui/src/mcp/client.ts +51 -0
- dcg/ui/src/mcp/hooks.ts +115 -0
- dcg/ui/src/mcp/mutations.ts +128 -0
- dcg/ui/src/mcp/static-client.ts +140 -0
- dcg/ui/src/mcp/types.ts +137 -0
- dcg/ui/src/store.ts +88 -0
- dcg/ui/tsconfig.json +20 -0
- dcg/ui/vite.config.ts +21 -0
- domain_context_graph-0.4.0.dist-info/METADATA +186 -0
- domain_context_graph-0.4.0.dist-info/RECORD +86 -0
- domain_context_graph-0.4.0.dist-info/WHEEL +4 -0
- domain_context_graph-0.4.0.dist-info/entry_points.txt +4 -0
dcg/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""DCG — Domain Context Graph."""
|
|
2
|
+
|
|
3
|
+
from dcg.core import (
|
|
4
|
+
entity_uid,
|
|
5
|
+
relation_uid,
|
|
6
|
+
Entity,
|
|
7
|
+
Relation,
|
|
8
|
+
SCHEMA_VERSION,
|
|
9
|
+
TYPE_REGISTRY,
|
|
10
|
+
PROPERTY_REGISTRY,
|
|
11
|
+
register_global_type,
|
|
12
|
+
register_property,
|
|
13
|
+
register_alias,
|
|
14
|
+
resolve_property,
|
|
15
|
+
GraphStore,
|
|
16
|
+
GraphStoreProtocol,
|
|
17
|
+
StackLoader,
|
|
18
|
+
DomainProject,
|
|
19
|
+
purge_retracted,
|
|
20
|
+
builders,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"entity_uid",
|
|
25
|
+
"relation_uid",
|
|
26
|
+
"Entity",
|
|
27
|
+
"Relation",
|
|
28
|
+
"SCHEMA_VERSION",
|
|
29
|
+
"TYPE_REGISTRY",
|
|
30
|
+
"PROPERTY_REGISTRY",
|
|
31
|
+
"register_global_type",
|
|
32
|
+
"register_property",
|
|
33
|
+
"register_alias",
|
|
34
|
+
"resolve_property",
|
|
35
|
+
"GraphStore",
|
|
36
|
+
"GraphStoreProtocol",
|
|
37
|
+
"StackLoader",
|
|
38
|
+
"DomainProject",
|
|
39
|
+
"purge_retracted",
|
|
40
|
+
"builders",
|
|
41
|
+
]
|
dcg/adapters/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""DCG adapters — pluggable backends for loading layers from external sources."""
|
|
2
|
+
from .base import AdapterProxy, LayerAdapterProtocol
|
|
3
|
+
from .graphify import GraphifyAdapter
|
|
4
|
+
from .registry import register
|
|
5
|
+
|
|
6
|
+
register("graphify", GraphifyAdapter)
|
|
7
|
+
|
|
8
|
+
__all__ = ["AdapterProxy", "GraphifyAdapter", "LayerAdapterProtocol"]
|
dcg/adapters/base.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Layer adapter protocol and enrichment proxy."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Protocol, runtime_checkable
|
|
8
|
+
|
|
9
|
+
from dcg.core.model import SCHEMA_VERSION
|
|
10
|
+
from dcg.core.store import GraphStore
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@runtime_checkable
|
|
14
|
+
class LayerAdapterProtocol(Protocol):
|
|
15
|
+
"""Reads an external source and populates a GraphStore."""
|
|
16
|
+
|
|
17
|
+
def load(self, source: Path, store: GraphStore, config: dict) -> None: ...
|
|
18
|
+
|
|
19
|
+
def source_exists(self, source: Path) -> bool: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _hash(obj: dict) -> str:
|
|
23
|
+
return hashlib.md5(json.dumps(obj, sort_keys=True, default=str).encode()).hexdigest()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_source_anchor(entity: dict) -> str | None:
|
|
27
|
+
for attr in entity.get("attributes", []):
|
|
28
|
+
if attr.get("property") == "source_anchor":
|
|
29
|
+
return attr.get("string")
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AdapterProxy:
|
|
34
|
+
"""Wraps a LayerAdapter with a DCG-native enrichment overlay.
|
|
35
|
+
|
|
36
|
+
Phase 1: adapter.load() populates store from external source.
|
|
37
|
+
Phase 2: overlay applies enrichments (patched entities, new relations).
|
|
38
|
+
On save: only the delta vs baseline is written to the overlay file.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
OVERLAY_DIR = ".dcg"
|
|
42
|
+
OVERLAY_FILE = "overlay.json"
|
|
43
|
+
|
|
44
|
+
def __init__(self, adapter: LayerAdapterProtocol) -> None:
|
|
45
|
+
self._adapter = adapter
|
|
46
|
+
self._baseline_entities: dict[str, str] = {}
|
|
47
|
+
self._baseline_relations: dict[str, str] = {}
|
|
48
|
+
self._anchor_index: dict[str, str] = {}
|
|
49
|
+
|
|
50
|
+
def load(self, source: Path, store: GraphStore, config: dict) -> None:
|
|
51
|
+
self._adapter.load(source, store, config)
|
|
52
|
+
|
|
53
|
+
self._snapshot_baseline(store)
|
|
54
|
+
|
|
55
|
+
overlay_path = source / self.OVERLAY_DIR / self.OVERLAY_FILE
|
|
56
|
+
if overlay_path.exists():
|
|
57
|
+
self._apply_overlay(store, overlay_path)
|
|
58
|
+
self._snapshot_baseline(store)
|
|
59
|
+
|
|
60
|
+
def _snapshot_baseline(self, store: GraphStore) -> None:
|
|
61
|
+
self._baseline_entities = {
|
|
62
|
+
uid: _hash(ent) for uid, ent in store._entities.items()
|
|
63
|
+
}
|
|
64
|
+
self._baseline_relations = {
|
|
65
|
+
uid: _hash(rel) for uid, rel in store._relations.items()
|
|
66
|
+
}
|
|
67
|
+
self._anchor_index = {}
|
|
68
|
+
for uid, ent in store._entities.items():
|
|
69
|
+
anchor = _get_source_anchor(ent)
|
|
70
|
+
if anchor:
|
|
71
|
+
self._anchor_index[anchor] = uid
|
|
72
|
+
|
|
73
|
+
def save_enrichments(self, source: Path, store: GraphStore) -> Path:
|
|
74
|
+
"""Write only the delta (enrichments) to the overlay file."""
|
|
75
|
+
overlay_path = source / self.OVERLAY_DIR / self.OVERLAY_FILE
|
|
76
|
+
|
|
77
|
+
enriched_entities: dict[str, dict] = {}
|
|
78
|
+
for uid, ent in store._entities.items():
|
|
79
|
+
if uid not in self._baseline_entities:
|
|
80
|
+
enriched_entities[uid] = ent
|
|
81
|
+
elif _hash(ent) != self._baseline_entities[uid]:
|
|
82
|
+
enriched_entities[uid] = ent
|
|
83
|
+
|
|
84
|
+
enriched_relations: dict[str, dict] = {}
|
|
85
|
+
for uid, rel in store._relations.items():
|
|
86
|
+
if uid not in self._baseline_relations:
|
|
87
|
+
enriched_relations[uid] = rel
|
|
88
|
+
|
|
89
|
+
overlay = {
|
|
90
|
+
"schema_version": SCHEMA_VERSION,
|
|
91
|
+
"entities": enriched_entities,
|
|
92
|
+
"relations": enriched_relations,
|
|
93
|
+
}
|
|
94
|
+
overlay_path.parent.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
overlay_path.write_text(json.dumps(overlay, indent=2, default=str))
|
|
96
|
+
return overlay_path
|
|
97
|
+
|
|
98
|
+
def has_enrichments(self, store: GraphStore) -> bool:
|
|
99
|
+
"""Return True if the store has changes beyond the adapter baseline."""
|
|
100
|
+
for uid, ent in store._entities.items():
|
|
101
|
+
if uid not in self._baseline_entities:
|
|
102
|
+
return True
|
|
103
|
+
if _hash(ent) != self._baseline_entities[uid]:
|
|
104
|
+
return True
|
|
105
|
+
for uid in store._relations:
|
|
106
|
+
if uid not in self._baseline_relations:
|
|
107
|
+
return True
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def _validate_overlay(data: dict, path: Path) -> None:
|
|
112
|
+
"""Validate overlay JSON has expected structure."""
|
|
113
|
+
if not isinstance(data, dict):
|
|
114
|
+
raise ValueError(f"Overlay at {path} must be a JSON object")
|
|
115
|
+
if "schema_version" not in data:
|
|
116
|
+
raise ValueError(f"Overlay at {path} missing 'schema_version'")
|
|
117
|
+
for key in ("entities", "relations"):
|
|
118
|
+
section = data.get(key)
|
|
119
|
+
if section is not None and not isinstance(section, dict):
|
|
120
|
+
raise ValueError(
|
|
121
|
+
f"Overlay at {path}: '{key}' must be an object mapping UIDs to dicts"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def _apply_overlay(self, store: GraphStore, path: Path) -> None:
|
|
125
|
+
"""Apply overlay with two-phase entity matching (R-023 ADAPT).
|
|
126
|
+
|
|
127
|
+
Phase 1: UID match — overlay entity UID exists in baseline → replace.
|
|
128
|
+
Phase 2: Source anchor fallback — overlay UID not in baseline, but
|
|
129
|
+
source_anchor matches a baseline entity → replace + rewrite UID.
|
|
130
|
+
No match: insert as new entity.
|
|
131
|
+
"""
|
|
132
|
+
data = json.loads(path.read_text())
|
|
133
|
+
self._validate_overlay(data, path)
|
|
134
|
+
uid_rewrites: dict[str, str] = {}
|
|
135
|
+
|
|
136
|
+
for overlay_uid, ent in data.get("entities", {}).items():
|
|
137
|
+
if overlay_uid in store._entities:
|
|
138
|
+
store.add_entity(ent)
|
|
139
|
+
else:
|
|
140
|
+
anchor = _get_source_anchor(ent)
|
|
141
|
+
if anchor and anchor in self._anchor_index:
|
|
142
|
+
baseline_uid = self._anchor_index[anchor]
|
|
143
|
+
ent["id"] = baseline_uid
|
|
144
|
+
store.add_entity(ent)
|
|
145
|
+
uid_rewrites[overlay_uid] = baseline_uid
|
|
146
|
+
else:
|
|
147
|
+
store.add_entity(ent)
|
|
148
|
+
|
|
149
|
+
for uid, rel in data.get("relations", {}).items():
|
|
150
|
+
source = rel.get("source", "")
|
|
151
|
+
target = rel.get("target", "")
|
|
152
|
+
source = uid_rewrites.get(source, source)
|
|
153
|
+
target = uid_rewrites.get(target, target)
|
|
154
|
+
prop = rel.get("property", "")
|
|
155
|
+
qualifiers = rel.get("qualifiers")
|
|
156
|
+
store.add_relation(source, target, prop, qualifiers)
|
dcg/adapters/graphify.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Graphify adapter — reads graphify-out/graph.json into a DCG GraphStore."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from dcg.core import builders as kb
|
|
10
|
+
from dcg.core.store import GraphStore
|
|
11
|
+
from dcg.core.uid import entity_uid, relation_uid
|
|
12
|
+
|
|
13
|
+
_log = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
GRAPH_FILE = "graph.json"
|
|
16
|
+
|
|
17
|
+
_DEFAULT_TYPE_MAP: dict[str, str] = {
|
|
18
|
+
"class": "Class",
|
|
19
|
+
"function": "Function",
|
|
20
|
+
"method": "Function",
|
|
21
|
+
"module": "Module",
|
|
22
|
+
"file": "File",
|
|
23
|
+
"interface": "Interface",
|
|
24
|
+
"struct": "Struct",
|
|
25
|
+
"endpoint": "Endpoint",
|
|
26
|
+
"trait": "Trait",
|
|
27
|
+
"package": "Module",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_DEFAULT_RELATION_MAP: dict[str, str] = {
|
|
31
|
+
"calls": "calls",
|
|
32
|
+
"imports": "imports",
|
|
33
|
+
"imports_from": "imports",
|
|
34
|
+
"inherits": "inherits from",
|
|
35
|
+
"contains": "contains",
|
|
36
|
+
"defines": "contains",
|
|
37
|
+
"method": "contains",
|
|
38
|
+
"uses": "depends on",
|
|
39
|
+
"depends_on": "depends on",
|
|
40
|
+
"extends": "inherits from",
|
|
41
|
+
"re_exports": "imports",
|
|
42
|
+
"references": "references",
|
|
43
|
+
"semantically_similar_to": "related to",
|
|
44
|
+
"rationale_for": "references",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_FILE_EXTENSIONS = frozenset({
|
|
48
|
+
".py", ".js", ".ts", ".jsx", ".tsx", ".sh", ".bash", ".go", ".rs",
|
|
49
|
+
".java", ".c", ".cpp", ".h", ".hpp", ".cs", ".rb", ".php", ".swift",
|
|
50
|
+
".kt", ".scala", ".vue", ".svelte", ".css", ".scss", ".html", ".json",
|
|
51
|
+
".yaml", ".yml", ".toml", ".xml", ".sql", ".proto", ".graphql",
|
|
52
|
+
".tf", ".hcl", ".r", ".jl", ".lua", ".zig", ".nim", ".ex", ".exs",
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
_LINE_RE = re.compile(r"L(\d+)")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _parse_line(loc: str) -> int | None:
|
|
59
|
+
m = _LINE_RE.match(loc or "")
|
|
60
|
+
return int(m.group(1)) if m else None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class GraphifyAdapter:
|
|
64
|
+
"""Reads a Graphify graph.json (NetworkX node_link_data) into a DCG GraphStore."""
|
|
65
|
+
|
|
66
|
+
def source_exists(self, source: Path) -> bool:
|
|
67
|
+
return (source / GRAPH_FILE).is_file()
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _infer_type(node: dict, type_map: dict[str, str]) -> str:
|
|
71
|
+
explicit = node.get("type", "")
|
|
72
|
+
if explicit and explicit.lower() in type_map:
|
|
73
|
+
return type_map[explicit.lower()]
|
|
74
|
+
|
|
75
|
+
kind = (node.get("metadata") or {}).get("kind", "")
|
|
76
|
+
if kind == "file":
|
|
77
|
+
return "File"
|
|
78
|
+
if kind in ("bash_entrypoint", "bash_function"):
|
|
79
|
+
return "Function"
|
|
80
|
+
if kind == "code":
|
|
81
|
+
return "Module"
|
|
82
|
+
|
|
83
|
+
label = node.get("label", "")
|
|
84
|
+
if not label:
|
|
85
|
+
return "Function"
|
|
86
|
+
|
|
87
|
+
if "." in label:
|
|
88
|
+
suffix = Path(label).suffix.lower()
|
|
89
|
+
if suffix in _FILE_EXTENSIONS:
|
|
90
|
+
return "File"
|
|
91
|
+
|
|
92
|
+
if label.endswith("()"):
|
|
93
|
+
return "Function"
|
|
94
|
+
|
|
95
|
+
if label.startswith("__") and label.endswith("__"):
|
|
96
|
+
return "Module"
|
|
97
|
+
|
|
98
|
+
if (label[0].isupper()
|
|
99
|
+
and any(c.islower() for c in label)
|
|
100
|
+
and "." not in label):
|
|
101
|
+
return "Class"
|
|
102
|
+
|
|
103
|
+
return "Function"
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def _extract_repo_name(graphify_repo: str) -> str:
|
|
107
|
+
"""Extract bare repo name (no org prefix) from a Graphify repo field."""
|
|
108
|
+
if "/" in graphify_repo:
|
|
109
|
+
return graphify_repo.rsplit("/", 1)[-1]
|
|
110
|
+
return graphify_repo
|
|
111
|
+
|
|
112
|
+
def load(self, source: Path, store: GraphStore, config: dict) -> None:
|
|
113
|
+
graph_path = source / GRAPH_FILE
|
|
114
|
+
if not graph_path.is_file():
|
|
115
|
+
_log.warning("Graphify graph not found: %s", graph_path)
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
data = json.loads(graph_path.read_text())
|
|
119
|
+
nodes = data.get("nodes", [])
|
|
120
|
+
links = data.get("links", [])
|
|
121
|
+
|
|
122
|
+
type_map = {**_DEFAULT_TYPE_MAP, **config.get("types", {})}
|
|
123
|
+
rel_map = {**_DEFAULT_RELATION_MAP, **config.get("relations", {})}
|
|
124
|
+
code_only = config.get("code_only", False)
|
|
125
|
+
|
|
126
|
+
self._bootstrap_ontology(store)
|
|
127
|
+
|
|
128
|
+
domain_name = config.get("domain", source.name)
|
|
129
|
+
domain_uid = entity_uid(name=domain_name)
|
|
130
|
+
store.add_entity(kb.entity(
|
|
131
|
+
uid=domain_uid,
|
|
132
|
+
label=domain_name,
|
|
133
|
+
description=f"Code entities from {domain_name}",
|
|
134
|
+
attributes=[
|
|
135
|
+
kb.attribute("instance of", kb.ref_value("dcg:meta:Domain")),
|
|
136
|
+
],
|
|
137
|
+
))
|
|
138
|
+
|
|
139
|
+
# Pass 1: nodes → entities
|
|
140
|
+
graphify_id_to_uid: dict[str, str] = {}
|
|
141
|
+
skipped = 0
|
|
142
|
+
|
|
143
|
+
for node in nodes:
|
|
144
|
+
label = node.get("label", "")
|
|
145
|
+
if not label:
|
|
146
|
+
skipped += 1
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
if code_only and node.get("file_type", "code") != "code":
|
|
150
|
+
skipped += 1
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
source_file = node.get("source_file", "")
|
|
154
|
+
line = _parse_line(node.get("source_location", ""))
|
|
155
|
+
repo = self._extract_repo_name(node.get("repo", ""))
|
|
156
|
+
|
|
157
|
+
identity: dict[str, str | int] = {"name": label}
|
|
158
|
+
if source_file:
|
|
159
|
+
identity["path"] = source_file
|
|
160
|
+
if repo:
|
|
161
|
+
identity["path"] = f"{repo}/{source_file}" if source_file else repo
|
|
162
|
+
uid = entity_uid(**identity)
|
|
163
|
+
|
|
164
|
+
dcg_type = self._infer_type(node, type_map)
|
|
165
|
+
type_uid = f"dcg:meta:{dcg_type}"
|
|
166
|
+
|
|
167
|
+
graphify_id = node.get("id", "")
|
|
168
|
+
anchor = graphify_id or label
|
|
169
|
+
if source_file:
|
|
170
|
+
anchor = f"{source_file}:{label}"
|
|
171
|
+
|
|
172
|
+
attrs = [
|
|
173
|
+
kb.attribute("instance of", kb.ref_value(type_uid)),
|
|
174
|
+
kb.attribute("part of", kb.ref_value(domain_uid)),
|
|
175
|
+
kb.attribute("source_anchor", kb.string_value(anchor)),
|
|
176
|
+
]
|
|
177
|
+
if source_file:
|
|
178
|
+
attrs.append(kb.attribute("file path", kb.string_value(source_file)))
|
|
179
|
+
if line is not None:
|
|
180
|
+
attrs.append(kb.attribute("line number", kb.quantity_value(line)))
|
|
181
|
+
if repo:
|
|
182
|
+
attrs.append(kb.attribute("repo", kb.string_value(repo)))
|
|
183
|
+
if node.get("confidence") is not None:
|
|
184
|
+
attrs.append(kb.attribute("confidence", kb.string_value(str(node["confidence"]))))
|
|
185
|
+
if node.get("community") is not None:
|
|
186
|
+
attrs.append(kb.attribute("community", kb.quantity_value(node["community"])))
|
|
187
|
+
|
|
188
|
+
store.add_entity(kb.entity(
|
|
189
|
+
uid=uid,
|
|
190
|
+
label=label,
|
|
191
|
+
description=node.get("description", ""),
|
|
192
|
+
attributes=attrs,
|
|
193
|
+
))
|
|
194
|
+
|
|
195
|
+
if graphify_id:
|
|
196
|
+
graphify_id_to_uid[graphify_id] = uid
|
|
197
|
+
|
|
198
|
+
# Pass 2: links → relations
|
|
199
|
+
rel_added = 0
|
|
200
|
+
for link in links:
|
|
201
|
+
src_uid = graphify_id_to_uid.get(link.get("source", ""))
|
|
202
|
+
tgt_uid = graphify_id_to_uid.get(link.get("target", ""))
|
|
203
|
+
if not src_uid or not tgt_uid:
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
graphify_rel = link.get("relation", "related to")
|
|
207
|
+
dcg_prop = rel_map.get(graphify_rel, "related to")
|
|
208
|
+
|
|
209
|
+
store.add_relation(src_uid, tgt_uid, dcg_prop)
|
|
210
|
+
rel_added += 1
|
|
211
|
+
|
|
212
|
+
_log.info(
|
|
213
|
+
"Graphify: loaded %d entities, %d relations (%d nodes skipped)",
|
|
214
|
+
len(graphify_id_to_uid), rel_added, skipped,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def _bootstrap_ontology(store: GraphStore) -> None:
|
|
219
|
+
pack_names = {p for p in store.ontology._loaded_packs} if hasattr(store.ontology, '_loaded_packs') else set()
|
|
220
|
+
if "code" not in pack_names:
|
|
221
|
+
store.ontology.load_pack("code")
|
|
222
|
+
|
|
223
|
+
for prop_name, datatype in [
|
|
224
|
+
("source_anchor", "string"),
|
|
225
|
+
("confidence", "string"),
|
|
226
|
+
("community", "quantity"),
|
|
227
|
+
("repo", "string"),
|
|
228
|
+
("related to", "ref"),
|
|
229
|
+
("depends on", "ref"),
|
|
230
|
+
("references", "ref"),
|
|
231
|
+
]:
|
|
232
|
+
if prop_name not in store.ontology.known_properties():
|
|
233
|
+
store.ontology.register_property(prop_name, datatype=datatype)
|
dcg/adapters/registry.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Adapter registry — maps adapter names to classes."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
_ADAPTERS: dict[str, type] = {}
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def register(name: str, adapter_cls: type) -> None:
|
|
8
|
+
_ADAPTERS[name] = adapter_cls
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get(name: str) -> type:
|
|
12
|
+
if name not in _ADAPTERS:
|
|
13
|
+
raise ValueError(f"Unknown adapter '{name}'. Available: {sorted(_ADAPTERS)}")
|
|
14
|
+
return _ADAPTERS[name]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def available() -> list[str]:
|
|
18
|
+
return sorted(_ADAPTERS)
|
dcg/cli.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""DCG — Domain Context Graph CLI."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _cmd_init(args: argparse.Namespace) -> None:
|
|
10
|
+
from dcg.core.project import DomainProject
|
|
11
|
+
|
|
12
|
+
proj = DomainProject(args.path)
|
|
13
|
+
proj.load()
|
|
14
|
+
if proj.is_valid():
|
|
15
|
+
print(json.dumps({"status": "already_initialized", **proj.project_info()}, indent=2))
|
|
16
|
+
return
|
|
17
|
+
proj.init(name=args.name, description=args.description or "")
|
|
18
|
+
print(json.dumps({"status": "initialized", **proj.project_info()}, indent=2))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _cmd_compose(args: argparse.Namespace) -> None:
|
|
22
|
+
from dcg.core.stack import StackLoader
|
|
23
|
+
|
|
24
|
+
stack = StackLoader(args.manifest)
|
|
25
|
+
stack.load()
|
|
26
|
+
print(json.dumps(stack.stack_info(), indent=2))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _cmd_purge(args: argparse.Namespace) -> None:
|
|
30
|
+
from dcg.core.project import DomainProject
|
|
31
|
+
from dcg.ops.lifecycle import purge
|
|
32
|
+
|
|
33
|
+
proj = DomainProject(args.path)
|
|
34
|
+
proj.load()
|
|
35
|
+
result = purge(proj)
|
|
36
|
+
print(json.dumps(result, indent=2))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _cmd_export_static(args: argparse.Namespace) -> None:
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
from dcg.core.stack import StackLoader
|
|
43
|
+
from dcg.ops.lifecycle import snapshot
|
|
44
|
+
from dcg.ops.ontology import list_ontology
|
|
45
|
+
|
|
46
|
+
stack = StackLoader(args.manifest)
|
|
47
|
+
stack.load()
|
|
48
|
+
|
|
49
|
+
# Merge ontology from all layers
|
|
50
|
+
merged_store = stack.project(stack.stack_info()["layers"][0]["name"]).store
|
|
51
|
+
raw_ont = list_ontology(store=merged_store)
|
|
52
|
+
ontology = {
|
|
53
|
+
"types": [
|
|
54
|
+
{"uid": uid, "label": uid.split(":")[-1], "description": desc}
|
|
55
|
+
for uid, desc in raw_ont.get("types", {}).items()
|
|
56
|
+
],
|
|
57
|
+
"properties": [
|
|
58
|
+
{"name": name, **info}
|
|
59
|
+
for name, info in raw_ont.get("properties", {}).items()
|
|
60
|
+
],
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
bundle = {
|
|
64
|
+
"snapshot": snapshot(merged_store),
|
|
65
|
+
"stack_info": stack.stack_info(),
|
|
66
|
+
"ontology": ontology,
|
|
67
|
+
"entities": stack.query(),
|
|
68
|
+
"relations": stack.get_relations(),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
output = Path(args.output)
|
|
72
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
output.write_text(json.dumps(bundle, default=str))
|
|
74
|
+
print(json.dumps({
|
|
75
|
+
"status": "exported",
|
|
76
|
+
"output": str(output),
|
|
77
|
+
"entities": len(bundle["entities"]),
|
|
78
|
+
"relations": len(bundle["relations"]),
|
|
79
|
+
}, indent=2))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _cmd_ui(args: argparse.Namespace) -> None:
|
|
83
|
+
if not args.path and not args.stack:
|
|
84
|
+
print("Error: provide a project path or --stack manifest", file=sys.stderr)
|
|
85
|
+
sys.exit(1)
|
|
86
|
+
try:
|
|
87
|
+
from dcg.ui import main as ui_main
|
|
88
|
+
except ImportError:
|
|
89
|
+
print("Error: dcg[ui] not installed. Run: pip install domain-context-graph[ui]", file=sys.stderr)
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
ui_main(
|
|
92
|
+
project_path=args.path,
|
|
93
|
+
stack_path=args.stack,
|
|
94
|
+
host=args.host,
|
|
95
|
+
port=args.port,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _cmd_ingest_mcp(args: argparse.Namespace) -> None:
|
|
100
|
+
from dcg.mcp.dcg_ingest_mcp import main as ingest_mcp_main
|
|
101
|
+
ingest_mcp_main()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _cmd_ingest(args: argparse.Namespace) -> None:
|
|
105
|
+
from dcg.ingest import run_ingest
|
|
106
|
+
|
|
107
|
+
report = run_ingest(
|
|
108
|
+
project_path=args.project,
|
|
109
|
+
seeds_dir=args.seeds,
|
|
110
|
+
dry_run=args.dry_run,
|
|
111
|
+
)
|
|
112
|
+
if report.errors:
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _cmd_query(args: argparse.Namespace) -> None:
|
|
117
|
+
from dcg.core.project import DomainProject
|
|
118
|
+
|
|
119
|
+
proj = DomainProject(args.path)
|
|
120
|
+
proj.load()
|
|
121
|
+
|
|
122
|
+
instance_of = None
|
|
123
|
+
if args.type:
|
|
124
|
+
instance_of = args.type if args.type.startswith("dcg:") else f"dcg:meta:{args.type}"
|
|
125
|
+
|
|
126
|
+
part_of = None
|
|
127
|
+
if args.domain:
|
|
128
|
+
from dcg.core import entity_uid
|
|
129
|
+
part_of = entity_uid(name=args.domain)
|
|
130
|
+
|
|
131
|
+
results = proj.store.query(instance_of=instance_of, part_of=part_of)
|
|
132
|
+
print(json.dumps(results, indent=2, default=str))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main() -> None:
|
|
136
|
+
parser = argparse.ArgumentParser(
|
|
137
|
+
prog="dcg",
|
|
138
|
+
description="Domain Context Graph — CLI for managing domain graph projects.",
|
|
139
|
+
)
|
|
140
|
+
sub = parser.add_subparsers(dest="command")
|
|
141
|
+
|
|
142
|
+
init_p = sub.add_parser("init", help="Create a new domain graph project")
|
|
143
|
+
init_p.add_argument("name", help="Project name")
|
|
144
|
+
init_p.add_argument("--path", default=".", help="Project directory (default: current)")
|
|
145
|
+
init_p.add_argument("--description", help="Project description")
|
|
146
|
+
|
|
147
|
+
compose_p = sub.add_parser("compose", help="Validate and inspect a stack manifest")
|
|
148
|
+
compose_p.add_argument("manifest", help="Path to dcg-stack.yml")
|
|
149
|
+
|
|
150
|
+
purge_p = sub.add_parser("purge", help="Remove retracted entities and relations")
|
|
151
|
+
purge_p.add_argument("path", help="Path to a DCG domain project directory")
|
|
152
|
+
|
|
153
|
+
query_p = sub.add_parser("query", help="Query entities in a project")
|
|
154
|
+
query_p.add_argument("path", help="Path to a DCG domain project directory")
|
|
155
|
+
query_p.add_argument("--type", help="Filter by entity type (e.g. Function, Domain)")
|
|
156
|
+
query_p.add_argument("--domain", help="Filter by domain name")
|
|
157
|
+
|
|
158
|
+
ingest_p = sub.add_parser("ingest", help="Ingest YAML seed files into a DCG project")
|
|
159
|
+
ingest_p.add_argument("--project", required=True, help="Path to DCG project directory")
|
|
160
|
+
ingest_p.add_argument("--seeds", default="seeds", help="Seed directory relative to project")
|
|
161
|
+
ingest_p.add_argument("--dry-run", action="store_true", help="Validate and report without writing")
|
|
162
|
+
|
|
163
|
+
ingest_mcp_p = sub.add_parser("ingest-mcp", help="Launch the DCG ingest (write) MCP server")
|
|
164
|
+
ingest_mcp_src = ingest_mcp_p.add_mutually_exclusive_group(required=True)
|
|
165
|
+
ingest_mcp_src.add_argument("--project", help="Path to a DCG domain project directory")
|
|
166
|
+
ingest_mcp_src.add_argument("--stack", help="Path to a dcg-stack.yml stack manifest")
|
|
167
|
+
ingest_mcp_p.add_argument("--layer", help="Initial active layer (default: first/topmost)")
|
|
168
|
+
ingest_mcp_p.add_argument("--no-strict", action="store_true", dest="no_strict",
|
|
169
|
+
help="Disable strict ontology validation")
|
|
170
|
+
ingest_mcp_p.add_argument("--http", action="store_true",
|
|
171
|
+
help="Use Streamable HTTP transport instead of stdio")
|
|
172
|
+
ingest_mcp_p.add_argument("--port", type=int, default=8000,
|
|
173
|
+
help="Port for HTTP transport (default: 8000)")
|
|
174
|
+
|
|
175
|
+
export_p = sub.add_parser("export-static", help="Export stack as static JSON bundle for offline UI")
|
|
176
|
+
export_p.add_argument("manifest", help="Path to dcg-stack.yml")
|
|
177
|
+
export_p.add_argument("--output", default="data.json", help="Output file path (default: data.json)")
|
|
178
|
+
|
|
179
|
+
ui_p = sub.add_parser("ui", help="Launch the graph explorer web UI")
|
|
180
|
+
ui_src = ui_p.add_mutually_exclusive_group()
|
|
181
|
+
ui_src.add_argument("path", nargs="?", default=None, help="Path to a DCG domain project directory")
|
|
182
|
+
ui_src.add_argument("--stack", help="Path to a dcg-stack.yml manifest")
|
|
183
|
+
ui_p.add_argument("--port", type=int, default=8080, help="Port for the web server (default: 8080)")
|
|
184
|
+
ui_p.add_argument("--host", default="localhost", help="Host for the web server (default: localhost)")
|
|
185
|
+
|
|
186
|
+
args = parser.parse_args()
|
|
187
|
+
|
|
188
|
+
if not args.command:
|
|
189
|
+
parser.print_help()
|
|
190
|
+
sys.exit(1)
|
|
191
|
+
|
|
192
|
+
commands = {
|
|
193
|
+
"init": _cmd_init,
|
|
194
|
+
"compose": _cmd_compose,
|
|
195
|
+
"purge": _cmd_purge,
|
|
196
|
+
"query": _cmd_query,
|
|
197
|
+
"ingest": _cmd_ingest,
|
|
198
|
+
"ingest-mcp": _cmd_ingest_mcp,
|
|
199
|
+
"export-static": _cmd_export_static,
|
|
200
|
+
"ui": _cmd_ui,
|
|
201
|
+
}
|
|
202
|
+
commands[args.command](args)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
if __name__ == "__main__":
|
|
206
|
+
main()
|