graphcoding 0.1.0__tar.gz → 0.2.0__tar.gz
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.
- {graphcoding-0.1.0/src/graphcoding.egg-info → graphcoding-0.2.0}/PKG-INFO +2 -3
- {graphcoding-0.1.0 → graphcoding-0.2.0}/README.md +1 -2
- {graphcoding-0.1.0 → graphcoding-0.2.0}/pyproject.toml +1 -1
- {graphcoding-0.1.0 → graphcoding-0.2.0}/src/graphcoding/__init__.py +1 -1
- {graphcoding-0.1.0 → graphcoding-0.2.0}/src/graphcoding/cli.py +17 -4
- {graphcoding-0.1.0 → graphcoding-0.2.0}/src/graphcoding/drift.py +3 -2
- {graphcoding-0.1.0 → graphcoding-0.2.0}/src/graphcoding/scan.py +5 -1
- {graphcoding-0.1.0 → graphcoding-0.2.0}/src/graphcoding/store.py +13 -0
- {graphcoding-0.1.0 → graphcoding-0.2.0}/src/graphcoding/sync.py +7 -2
- {graphcoding-0.1.0 → graphcoding-0.2.0/src/graphcoding.egg-info}/PKG-INFO +2 -3
- {graphcoding-0.1.0 → graphcoding-0.2.0}/tests/test_e2e.py +34 -0
- {graphcoding-0.1.0 → graphcoding-0.2.0}/LICENSE +0 -0
- {graphcoding-0.1.0 → graphcoding-0.2.0}/setup.cfg +0 -0
- {graphcoding-0.1.0 → graphcoding-0.2.0}/src/graphcoding/health.py +0 -0
- {graphcoding-0.1.0 → graphcoding-0.2.0}/src/graphcoding/hooks.py +0 -0
- {graphcoding-0.1.0 → graphcoding-0.2.0}/src/graphcoding.egg-info/SOURCES.txt +0 -0
- {graphcoding-0.1.0 → graphcoding-0.2.0}/src/graphcoding.egg-info/dependency_links.txt +0 -0
- {graphcoding-0.1.0 → graphcoding-0.2.0}/src/graphcoding.egg-info/entry_points.txt +0 -0
- {graphcoding-0.1.0 → graphcoding-0.2.0}/src/graphcoding.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graphcoding
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Draw the graph of the system you want — then code until the repo matches. Future files and scheduled deletions are graph data; a drift gate blocks commits until code and declared design converge.
|
|
5
5
|
Author: Mosab Sayyed
|
|
6
6
|
License: MIT
|
|
@@ -72,8 +72,6 @@ GraphCoding is that layer, with a gate on it — and only that layer, on purpose
|
|
|
72
72
|
|
|
73
73
|
```bash
|
|
74
74
|
pip install graphcoding # or: pipx install graphcoding
|
|
75
|
-
# until the first PyPI release lands:
|
|
76
|
-
# pip install git+https://github.com/mosabsayyed/graphcoding
|
|
77
75
|
|
|
78
76
|
cd your-repo
|
|
79
77
|
graphcoding init --hooks # scan the repo into a graph + install the gate
|
|
@@ -154,6 +152,7 @@ The [playbooks](docs/playbooks.md) give the exact command sequence for each case
|
|
|
154
152
|
| [Core concepts](docs/core-concepts.md) | nodes, edges, statuses, the graph file format |
|
|
155
153
|
| [The loop](docs/lifecycle.md) | QUERY → DESIGN → CODE → SYNC → VERIFY, in depth |
|
|
156
154
|
| [Playbooks](docs/playbooks.md) | every SDLC situation, exact commands |
|
|
155
|
+
| [The whole architecture](docs/whole-system-graph.md) | db schema, settings tables, MCP servers, queues, external APIs as graph nodes |
|
|
157
156
|
| [Migrating an existing repo](docs/migrating-existing-repos.md) | zero to gated in an afternoon |
|
|
158
157
|
| [Starting a new project](docs/starting-new-projects.md) | graph-first greenfield |
|
|
159
158
|
| [AI agents](docs/agents.md) | Claude Code, Cursor, Copilot, custom harnesses |
|
|
@@ -51,8 +51,6 @@ GraphCoding is that layer, with a gate on it — and only that layer, on purpose
|
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
53
|
pip install graphcoding # or: pipx install graphcoding
|
|
54
|
-
# until the first PyPI release lands:
|
|
55
|
-
# pip install git+https://github.com/mosabsayyed/graphcoding
|
|
56
54
|
|
|
57
55
|
cd your-repo
|
|
58
56
|
graphcoding init --hooks # scan the repo into a graph + install the gate
|
|
@@ -133,6 +131,7 @@ The [playbooks](docs/playbooks.md) give the exact command sequence for each case
|
|
|
133
131
|
| [Core concepts](docs/core-concepts.md) | nodes, edges, statuses, the graph file format |
|
|
134
132
|
| [The loop](docs/lifecycle.md) | QUERY → DESIGN → CODE → SYNC → VERIFY, in depth |
|
|
135
133
|
| [Playbooks](docs/playbooks.md) | every SDLC situation, exact commands |
|
|
134
|
+
| [The whole architecture](docs/whole-system-graph.md) | db schema, settings tables, MCP servers, queues, external APIs as graph nodes |
|
|
136
135
|
| [Migrating an existing repo](docs/migrating-existing-repos.md) | zero to gated in an afternoon |
|
|
137
136
|
| [Starting a new project](docs/starting-new-projects.md) | graph-first greenfield |
|
|
138
137
|
| [AI agents](docs/agents.md) | Claude Code, Cursor, Copilot, custom harnesses |
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "graphcoding"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Draw the graph of the system you want — then code until the repo matches. Future files and scheduled deletions are graph data; a drift gate blocks commits until code and declared design converge."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -100,7 +100,8 @@ def cmd_scan(args) -> None:
|
|
|
100
100
|
def cmd_plan(args) -> None:
|
|
101
101
|
root = _root_or_die(args)
|
|
102
102
|
g = Graph.load(root)
|
|
103
|
-
|
|
103
|
+
status = "ok" if args.existing else "planned"
|
|
104
|
+
node = Node(name=args.name, type=args.type, status=status,
|
|
104
105
|
summary=args.summary or "")
|
|
105
106
|
for spec in args.edge or []:
|
|
106
107
|
try:
|
|
@@ -116,7 +117,7 @@ def cmd_plan(args) -> None:
|
|
|
116
117
|
sys.exit(f"{args.name} already exists with status={existing.status}; "
|
|
117
118
|
"use --force to re-plan it")
|
|
118
119
|
if existing:
|
|
119
|
-
existing.status =
|
|
120
|
+
existing.status = status
|
|
120
121
|
if args.summary:
|
|
121
122
|
existing.summary = args.summary
|
|
122
123
|
for e in node.edges:
|
|
@@ -124,7 +125,8 @@ def cmd_plan(args) -> None:
|
|
|
124
125
|
else:
|
|
125
126
|
g.nodes[node.name] = node
|
|
126
127
|
g.save()
|
|
127
|
-
|
|
128
|
+
verb = "recorded (existing)" if args.existing else "planned"
|
|
129
|
+
print(f"{verb}: {args.name}" + (f" — {args.summary}" if args.summary else ""))
|
|
128
130
|
|
|
129
131
|
|
|
130
132
|
def cmd_link(args) -> None:
|
|
@@ -156,6 +158,13 @@ def cmd_mark_delete(args) -> None:
|
|
|
156
158
|
print(f" <-[{t}]- {s}")
|
|
157
159
|
sys.exit("refusing to mark for deletion — update the callers first, "
|
|
158
160
|
"or pass --force if they are part of the same removal")
|
|
161
|
+
from .store import is_external
|
|
162
|
+
if is_external(args.name, load_config(root)):
|
|
163
|
+
# nothing on disk to remove — the declaration itself is retired
|
|
164
|
+
g.delete(args.name)
|
|
165
|
+
g.save()
|
|
166
|
+
print(f"removed external node: {args.name}")
|
|
167
|
+
return
|
|
159
168
|
node.status = "to-be-deleted"
|
|
160
169
|
g.save()
|
|
161
170
|
print(f"marked to-be-deleted: {args.name} "
|
|
@@ -314,11 +323,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
314
323
|
s.set_defaults(func=cmd_scan)
|
|
315
324
|
|
|
316
325
|
s = sub.add_parser("plan", help="declare a node you intend to build (DESIGN)")
|
|
317
|
-
s.add_argument("name", help="repo-relative path,
|
|
326
|
+
s.add_argument("name", help="repo-relative path, path::Symbol, or an external "
|
|
327
|
+
"name like db:orders / api:stripe (see external_prefixes)")
|
|
318
328
|
s.add_argument("--summary", "-s", help="one line: what it will do")
|
|
319
329
|
s.add_argument("--type", "-t", default="CodeFile", choices=NODE_TYPES)
|
|
320
330
|
s.add_argument("--edge", "-e", action="append",
|
|
321
331
|
help="TYPE:target (repeatable), e.g. -e IMPORTS:src/db.py")
|
|
332
|
+
s.add_argument("--existing", action="store_true",
|
|
333
|
+
help="record something that already exists (status ok) — "
|
|
334
|
+
"e.g. a live db table or external API")
|
|
322
335
|
s.add_argument("--force", action="store_true")
|
|
323
336
|
s.set_defaults(func=cmd_plan)
|
|
324
337
|
|
|
@@ -14,12 +14,13 @@ from __future__ import annotations
|
|
|
14
14
|
import os
|
|
15
15
|
|
|
16
16
|
from .scan import tracked_files
|
|
17
|
-
from .store import Graph
|
|
17
|
+
from .store import Graph, is_external
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def compute_drift(root: str, cfg: dict, graph: Graph) -> dict:
|
|
21
21
|
disk = set(tracked_files(root, cfg))
|
|
22
|
-
file_nodes = graph.file_nodes()
|
|
22
|
+
file_nodes = {k: v for k, v in graph.file_nodes().items()
|
|
23
|
+
if not is_external(k, cfg)}
|
|
23
24
|
missing = sorted(disk - set(file_nodes))
|
|
24
25
|
ghosts, unbuilt, not_deleted = [], [], []
|
|
25
26
|
for path, node in sorted(file_nodes.items()):
|
|
@@ -235,13 +235,17 @@ def scan_repo(root: str, cfg: dict, graph: Graph) -> dict:
|
|
|
235
235
|
node, subs = scan_file(root, path, cfg)
|
|
236
236
|
old = graph.nodes.get(path)
|
|
237
237
|
if old:
|
|
238
|
-
# never clobber intent: keep richer summary
|
|
238
|
+
# never clobber intent: keep richer summary, lifecycle statuses,
|
|
239
|
+
# and every hand-recorded edge (scanner owns IMPORTS only)
|
|
239
240
|
if old.summary and not node.summary:
|
|
240
241
|
node.summary = old.summary
|
|
241
242
|
if len(old.summary) > len(node.summary):
|
|
242
243
|
node.summary = old.summary
|
|
243
244
|
if old.status == "to-be-deleted":
|
|
244
245
|
node.status = old.status
|
|
246
|
+
for e in old.edges:
|
|
247
|
+
if e["type"] != "IMPORTS":
|
|
248
|
+
node.add_edge(e["to"], e["type"])
|
|
245
249
|
updated += 1
|
|
246
250
|
else:
|
|
247
251
|
added += 1
|
|
@@ -52,9 +52,22 @@ DEFAULT_CONFIG = {
|
|
|
52
52
|
],
|
|
53
53
|
"ignore_tests": True,
|
|
54
54
|
"scan_symbols": False,
|
|
55
|
+
# nodes whose names start with these prefixes describe architecture that
|
|
56
|
+
# is not a repo file — drift never expects one on disk:
|
|
57
|
+
# db: database objects (db:orders, db:settings::llm_provider)
|
|
58
|
+
# mcp: MCP servers and their tools (mcp:router::get_blast_radius)
|
|
59
|
+
# svc: deployed services / processes (svc:api-gateway)
|
|
60
|
+
# queue: queues / topics (queue:invoice-events)
|
|
61
|
+
# api: third-party APIs (api:stripe::charges)
|
|
62
|
+
# ext: anything else outside the repo
|
|
63
|
+
"external_prefixes": ["db:", "mcp:", "svc:", "queue:", "api:", "ext:"],
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
|
|
67
|
+
def is_external(name: str, cfg: dict) -> bool:
|
|
68
|
+
return any(name.startswith(p) for p in cfg.get("external_prefixes", []))
|
|
69
|
+
|
|
70
|
+
|
|
58
71
|
@dataclass
|
|
59
72
|
class Node:
|
|
60
73
|
name: str
|
|
@@ -73,8 +73,13 @@ def sync(root: str, cfg: dict, graph: Graph,
|
|
|
73
73
|
else:
|
|
74
74
|
node, subs = scan_file(root, path, cfg)
|
|
75
75
|
old = graph.nodes.get(path)
|
|
76
|
-
if old
|
|
77
|
-
|
|
76
|
+
if old:
|
|
77
|
+
if len(old.summary) > len(node.summary):
|
|
78
|
+
node.summary = old.summary
|
|
79
|
+
# scanner owns IMPORTS; every hand-recorded edge type survives
|
|
80
|
+
for e in old.edges:
|
|
81
|
+
if e["type"] != "IMPORTS":
|
|
82
|
+
node.add_edge(e["to"], e["type"])
|
|
78
83
|
graph.nodes[path] = node
|
|
79
84
|
for s in subs:
|
|
80
85
|
prev = graph.nodes.get(s.name)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graphcoding
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Draw the graph of the system you want — then code until the repo matches. Future files and scheduled deletions are graph data; a drift gate blocks commits until code and declared design converge.
|
|
5
5
|
Author: Mosab Sayyed
|
|
6
6
|
License: MIT
|
|
@@ -72,8 +72,6 @@ GraphCoding is that layer, with a gate on it — and only that layer, on purpose
|
|
|
72
72
|
|
|
73
73
|
```bash
|
|
74
74
|
pip install graphcoding # or: pipx install graphcoding
|
|
75
|
-
# until the first PyPI release lands:
|
|
76
|
-
# pip install git+https://github.com/mosabsayyed/graphcoding
|
|
77
75
|
|
|
78
76
|
cd your-repo
|
|
79
77
|
graphcoding init --hooks # scan the repo into a graph + install the gate
|
|
@@ -154,6 +152,7 @@ The [playbooks](docs/playbooks.md) give the exact command sequence for each case
|
|
|
154
152
|
| [Core concepts](docs/core-concepts.md) | nodes, edges, statuses, the graph file format |
|
|
155
153
|
| [The loop](docs/lifecycle.md) | QUERY → DESIGN → CODE → SYNC → VERIFY, in depth |
|
|
156
154
|
| [Playbooks](docs/playbooks.md) | every SDLC situation, exact commands |
|
|
155
|
+
| [The whole architecture](docs/whole-system-graph.md) | db schema, settings tables, MCP servers, queues, external APIs as graph nodes |
|
|
157
156
|
| [Migrating an existing repo](docs/migrating-existing-repos.md) | zero to gated in an afternoon |
|
|
158
157
|
| [Starting a new project](docs/starting-new-projects.md) | graph-first greenfield |
|
|
159
158
|
| [AI agents](docs/agents.md) | Claude Code, Cursor, Copilot, custom harnesses |
|
|
@@ -206,6 +206,40 @@ def test_show_discloses_recorded_only(repo, capsys):
|
|
|
206
206
|
assert "scanner-visible edges only" not in capsys.readouterr().out
|
|
207
207
|
|
|
208
208
|
|
|
209
|
+
def test_manual_edges_survive_rescan_and_sync(repo):
|
|
210
|
+
run(repo, "init")
|
|
211
|
+
run(repo, "link", "src/app.py", "CALLS", "db:orders")
|
|
212
|
+
# edit the file -> sync rescans it; the hand-recorded edge must survive
|
|
213
|
+
with open(os.path.join(repo, "src", "app.py"), "a") as f:
|
|
214
|
+
f.write("\nX = 1\n")
|
|
215
|
+
run(repo, "sync", "--files", "src/app.py")
|
|
216
|
+
g = Graph.load(repo)
|
|
217
|
+
assert {"to": "db:orders", "type": "CALLS"} in g.nodes["src/app.py"].edges
|
|
218
|
+
run(repo, "scan") # full rescan must preserve it too
|
|
219
|
+
g = Graph.load(repo)
|
|
220
|
+
assert {"to": "db:orders", "type": "CALLS"} in g.nodes["src/app.py"].edges
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_external_nodes_db_mcp(repo, capsys):
|
|
224
|
+
run(repo, "init")
|
|
225
|
+
run(repo, "plan", "db:orders", "--existing", "-t", "ServiceDef",
|
|
226
|
+
"-s", "Order ledger; written only by app")
|
|
227
|
+
run(repo, "plan", "mcp:router::search", "--existing", "-t", "ServiceDef",
|
|
228
|
+
"-s", "Semantic search tool")
|
|
229
|
+
g = Graph.load(repo)
|
|
230
|
+
assert g.nodes["db:orders"].status == "ok"
|
|
231
|
+
run(repo, "drift", expect_exit=0) # externals are never ghosts
|
|
232
|
+
run(repo, "link", "src/app.py", "CALLS", "db:orders")
|
|
233
|
+
capsys.readouterr()
|
|
234
|
+
run(repo, "show", "db:orders")
|
|
235
|
+
assert "src/app.py" in capsys.readouterr().out
|
|
236
|
+
run(repo, "mark-delete", "db:orders", expect_exit=1) # caller recorded
|
|
237
|
+
run(repo, "mark-delete", "mcp:router::search", expect_exit=0)
|
|
238
|
+
g = Graph.load(repo)
|
|
239
|
+
assert "mcp:router::search" not in g.nodes # externals retire immediately
|
|
240
|
+
run(repo, "drift", expect_exit=0)
|
|
241
|
+
|
|
242
|
+
|
|
209
243
|
def test_graph_file_is_sorted_and_stable(repo):
|
|
210
244
|
run(repo, "init")
|
|
211
245
|
p = os.path.join(repo, ".graphcoding", "graph.jsonl")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|