graphcoding 0.1.0__tar.gz → 0.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphcoding
3
- Version: 0.1.0
3
+ Version: 0.3.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.1.0"
7
+ version = "0.3.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"
@@ -3,4 +3,4 @@
3
3
  Query before you touch code. Design in the graph. Sync as you go.
4
4
  Drift is caught by tooling, not memory.
5
5
  """
6
- __version__ = "0.1.0"
6
+ __version__ = "0.3.0"
@@ -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
- node = Node(name=args.name, type=args.type, status="planned",
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 = "planned"
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
- print(f"planned: {args.name}" + (f" — {args.summary}" if args.summary else ""))
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,16 @@ 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, or path::Symbol")
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
- s.add_argument("--type", "-t", default="CodeFile", choices=NODE_TYPES)
329
+ s.add_argument("--type", "-t", default="CodeFile",
330
+ help=f"free-form; common: {', '.join(NODE_TYPES)}")
320
331
  s.add_argument("--edge", "-e", action="append",
321
332
  help="TYPE:target (repeatable), e.g. -e IMPORTS:src/db.py")
333
+ s.add_argument("--existing", action="store_true",
334
+ help="record something that already exists (status ok) — "
335
+ "e.g. a live db table or external API")
322
336
  s.add_argument("--force", action="store_true")
323
337
  s.set_defaults(func=cmd_plan)
324
338
 
@@ -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 and lifecycle statuses
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
@@ -21,6 +21,7 @@ from __future__ import annotations
21
21
 
22
22
  import json
23
23
  import os
24
+ import re
24
25
  from dataclasses import dataclass, field
25
26
 
26
27
  NODE_TYPES = [
@@ -54,6 +55,19 @@ DEFAULT_CONFIG = {
54
55
  "scan_symbols": False,
55
56
  }
56
57
 
58
+ # The classification is OPEN and binary: a node is either CODE (a repo-relative
59
+ # file path — scanned, drift-gated) or ANOTHER SYSTEM (any "scheme:" name —
60
+ # declared, never expected on disk). Invent whatever schemes fit your world:
61
+ # db:orders, mcp:router::search, svc:gateway, erp:sap::orders, team:payments,
62
+ # sensor:plant-7. The scheme is yours; the lifecycle and edges are the same.
63
+ _SCHEME = re.compile(r"^[A-Za-z][A-Za-z0-9_.+-]*:(?!//)")
64
+
65
+
66
+ def is_external(name: str, cfg: dict | None = None) -> bool:
67
+ """True for 'scheme:...' names (non-file architecture). File paths never
68
+ carry a scheme; URLs (scheme://) are also treated as external."""
69
+ return bool(_SCHEME.match(name)) or "://" in name
70
+
57
71
 
58
72
  @dataclass
59
73
  class Node:
@@ -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 and len(old.summary) > len(node.summary):
77
- node.summary = old.summary
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.1.0
3
+ Version: 0.3.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,47 @@ 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
+ # the classification is OPEN — any invented scheme and type work
242
+ run(repo, "plan", "erp:sap::orders", "--existing", "-t", "ErpObject",
243
+ "-s", "SAP order master; synced nightly")
244
+ run(repo, "link", "src/app.py", "REFERENCES", "erp:sap::orders")
245
+ run(repo, "drift", expect_exit=0)
246
+ g = Graph.load(repo)
247
+ assert g.nodes["erp:sap::orders"].type == "ErpObject"
248
+
249
+
209
250
  def test_graph_file_is_sorted_and_stable(repo):
210
251
  run(repo, "init")
211
252
  p = os.path.join(repo, ".graphcoding", "graph.jsonl")
File without changes
File without changes