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.
Files changed (86) hide show
  1. dcg/__init__.py +41 -0
  2. dcg/adapters/__init__.py +8 -0
  3. dcg/adapters/base.py +156 -0
  4. dcg/adapters/graphify.py +233 -0
  5. dcg/adapters/registry.py +18 -0
  6. dcg/cli.py +206 -0
  7. dcg/core/__init__.py +63 -0
  8. dcg/core/builders.py +68 -0
  9. dcg/core/model.py +113 -0
  10. dcg/core/ontology.py +386 -0
  11. dcg/core/packs/code.yaml +27 -0
  12. dcg/core/packs/core.yaml +65 -0
  13. dcg/core/packs/dcg-development.yaml +22 -0
  14. dcg/core/packs/security.yaml +9 -0
  15. dcg/core/project.py +247 -0
  16. dcg/core/purge.py +9 -0
  17. dcg/core/stack.py +643 -0
  18. dcg/core/store.py +408 -0
  19. dcg/core/uid.py +28 -0
  20. dcg/ingest/__init__.py +58 -0
  21. dcg/ingest/discovery.py +69 -0
  22. dcg/ingest/models.py +132 -0
  23. dcg/ingest/report.py +47 -0
  24. dcg/ingest/runner.py +295 -0
  25. dcg/mcp/__init__.py +6 -0
  26. dcg/mcp/__main__.py +4 -0
  27. dcg/mcp/dcg_ingest_mcp.py +89 -0
  28. dcg/mcp/dcg_mcp.py +79 -0
  29. dcg/mcp/helpers.py +12 -0
  30. dcg/mcp/state.py +20 -0
  31. dcg/mcp/tools/__init__.py +1 -0
  32. dcg/mcp/tools/ingest.py +161 -0
  33. dcg/mcp/tools/project.py +37 -0
  34. dcg/mcp/tools/query.py +145 -0
  35. dcg/mcp/tools/stack.py +99 -0
  36. dcg/ops/__init__.py +1 -0
  37. dcg/ops/ingest.py +167 -0
  38. dcg/ops/lifecycle.py +86 -0
  39. dcg/ops/ontology.py +37 -0
  40. dcg/ui/__init__.py +116 -0
  41. dcg/ui/index.html +12 -0
  42. dcg/ui/package-lock.json +5172 -0
  43. dcg/ui/package.json +41 -0
  44. dcg/ui/src/App.tsx +35 -0
  45. dcg/ui/src/__tests__/store.test.ts +65 -0
  46. dcg/ui/src/components/AppShell.tsx +45 -0
  47. dcg/ui/src/components/AttributeRow.tsx +16 -0
  48. dcg/ui/src/components/CreateEntityDialog.tsx +82 -0
  49. dcg/ui/src/components/DetailPanel.tsx +20 -0
  50. dcg/ui/src/components/DomainFilter.tsx +65 -0
  51. dcg/ui/src/components/EntityDetail.tsx +229 -0
  52. dcg/ui/src/components/EntityForm.tsx +117 -0
  53. dcg/ui/src/components/GraphCanvas.tsx +119 -0
  54. dcg/ui/src/components/LayerDetail.tsx +53 -0
  55. dcg/ui/src/components/LayerSwitcher.tsx +34 -0
  56. dcg/ui/src/components/RelationDetail.tsx +55 -0
  57. dcg/ui/src/components/RelationRow.tsx +43 -0
  58. dcg/ui/src/components/SearchInput.tsx +26 -0
  59. dcg/ui/src/components/Sidebar.tsx +13 -0
  60. dcg/ui/src/components/StackTopology.tsx +147 -0
  61. dcg/ui/src/components/StatusBar.tsx +24 -0
  62. dcg/ui/src/components/Toolbar.tsx +88 -0
  63. dcg/ui/src/components/TypeFilter.tsx +36 -0
  64. dcg/ui/src/components/ViewModeToggle.tsx +30 -0
  65. dcg/ui/src/declarations.d.ts +5 -0
  66. dcg/ui/src/graph/__tests__/transform.test.ts +114 -0
  67. dcg/ui/src/graph/layouts.ts +36 -0
  68. dcg/ui/src/graph/styles.ts +90 -0
  69. dcg/ui/src/graph/transform.ts +140 -0
  70. dcg/ui/src/index.css +1 -0
  71. dcg/ui/src/main.tsx +10 -0
  72. dcg/ui/src/mcp/McpProvider.tsx +57 -0
  73. dcg/ui/src/mcp/StaticProvider.tsx +24 -0
  74. dcg/ui/src/mcp/__tests__/types.test.ts +45 -0
  75. dcg/ui/src/mcp/client.ts +51 -0
  76. dcg/ui/src/mcp/hooks.ts +115 -0
  77. dcg/ui/src/mcp/mutations.ts +128 -0
  78. dcg/ui/src/mcp/static-client.ts +140 -0
  79. dcg/ui/src/mcp/types.ts +137 -0
  80. dcg/ui/src/store.ts +88 -0
  81. dcg/ui/tsconfig.json +20 -0
  82. dcg/ui/vite.config.ts +21 -0
  83. domain_context_graph-0.4.0.dist-info/METADATA +186 -0
  84. domain_context_graph-0.4.0.dist-info/RECORD +86 -0
  85. domain_context_graph-0.4.0.dist-info/WHEEL +4 -0
  86. 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
+ ]
@@ -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)
@@ -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)
@@ -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()