vibesequence 0.1.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.
Files changed (31) hide show
  1. vibesequence-0.1.0/LICENSE +21 -0
  2. vibesequence-0.1.0/PKG-INFO +112 -0
  3. vibesequence-0.1.0/README.md +90 -0
  4. vibesequence-0.1.0/pyproject.toml +38 -0
  5. vibesequence-0.1.0/setup.cfg +4 -0
  6. vibesequence-0.1.0/tests/test_flow.py +179 -0
  7. vibesequence-0.1.0/vibesequence/__init__.py +0 -0
  8. vibesequence-0.1.0/vibesequence/__main__.py +2 -0
  9. vibesequence-0.1.0/vibesequence/builder.py +179 -0
  10. vibesequence-0.1.0/vibesequence/cli.py +309 -0
  11. vibesequence-0.1.0/vibesequence/editor.py +71 -0
  12. vibesequence-0.1.0/vibesequence/entrypoints.py +106 -0
  13. vibesequence-0.1.0/vibesequence/extractors/__init__.py +0 -0
  14. vibesequence-0.1.0/vibesequence/extractors/base.py +34 -0
  15. vibesequence-0.1.0/vibesequence/extractors/python_ast.py +257 -0
  16. vibesequence-0.1.0/vibesequence/extractors/typescript_ts.py +497 -0
  17. vibesequence-0.1.0/vibesequence/flow.py +222 -0
  18. vibesequence-0.1.0/vibesequence/model.py +97 -0
  19. vibesequence-0.1.0/vibesequence/render/__init__.py +0 -0
  20. vibesequence-0.1.0/vibesequence/render/html.py +27 -0
  21. vibesequence-0.1.0/vibesequence/render/mermaid.py +123 -0
  22. vibesequence-0.1.0/vibesequence/render/text.py +67 -0
  23. vibesequence-0.1.0/vibesequence/render/viewer.html +1195 -0
  24. vibesequence-0.1.0/vibesequence/resolver.py +211 -0
  25. vibesequence-0.1.0/vibesequence/serve.py +119 -0
  26. vibesequence-0.1.0/vibesequence.egg-info/PKG-INFO +112 -0
  27. vibesequence-0.1.0/vibesequence.egg-info/SOURCES.txt +29 -0
  28. vibesequence-0.1.0/vibesequence.egg-info/dependency_links.txt +1 -0
  29. vibesequence-0.1.0/vibesequence.egg-info/entry_points.txt +2 -0
  30. vibesequence-0.1.0/vibesequence.egg-info/requires.txt +4 -0
  31. vibesequence-0.1.0/vibesequence.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gery Wibowo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: vibesequence
3
+ Version: 0.1.0
4
+ Summary: Turn a codebase into zoomable user sequence diagrams, for you and your AI assistant
5
+ Author: Gery Wibowo
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/monkeymars/vibesequence
8
+ Project-URL: Documentation, https://github.com/monkeymars/vibesequence/blob/main/docs/how-it-works.md
9
+ Project-URL: Issues, https://github.com/monkeymars/vibesequence/issues
10
+ Keywords: sequence-diagram,code-visualization,mermaid,static-analysis,ai-assistant
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Topic :: Software Development :: Documentation
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Provides-Extra: ts
19
+ Requires-Dist: tree-sitter>=0.23; extra == "ts"
20
+ Requires-Dist: tree-sitter-typescript>=0.23; extra == "ts"
21
+ Dynamic: license-file
22
+
23
+ # vibesequence
24
+
25
+ Turn your codebase into **zoomable sequence diagrams** that show *when a user does X, who calls whom,
26
+ and in what order*. Use them as a map while you build, review or learn the code, on your own or with an
27
+ AI assistant.
28
+
29
+ Inspired by [graphify](https://github.com/Graphify-Labs/graphify), which turns a codebase into a knowledge
30
+ graph so an AI assistant can navigate it without re-reading every file. vibesequence takes the same idea
31
+ and adds what a graph leaves out: the **order** of calls and the branches around them (`if`, loops, error
32
+ handling), drawn as user sequence diagrams that you and the assistant can both read.
33
+
34
+ ![vibesequence demo](https://raw.githubusercontent.com/monkeymars/vibesequence/main/demo/vibesequence-demo.gif)
35
+
36
+ ## Why
37
+
38
+ When an AI assistant edits your code, it is hard to see what actually changed in the behavior.
39
+ vibesequence reads your code (no running it), finds the entry points (HTTP routes, CLI commands, handlers),
40
+ and draws each one as a sequence diagram:
41
+
42
+ - **Zoom** between package, module, class and function level. Click a blue call to open it on its own.
43
+ - **Play** a flow arrow by arrow, with the `file:line` of each step.
44
+ - **Live**: while the AI edits, changed steps turn green and you can play only the changes.
45
+ - **AI friendly**: the same flow is available as compact text, so your assistant reads one diagram instead of eight files.
46
+
47
+ Supports Python (stdlib only) and TypeScript / JavaScript (Next.js, Express).
48
+
49
+ ## Does it save tokens?
50
+
51
+ We benchmarked Claude Code on the same tasks with and without vibesequence. Every run passed in both setups.
52
+
53
+ | task type | input tokens | cost | turns |
54
+ |---|---|---|---|
55
+ | understanding a flow ("what happens when…", "what breaks if I change…") | **−23%** | **−18%** | 8 → 4 |
56
+ | small edits | +19% | +15% | 5.5 → 6 |
57
+
58
+ Impact questions save the most: one `vibesequence callers` call replaced 6–7 greps and reads (−53% tokens).
59
+ For edits, the agent still has to read the files it changes, so on the tiny example projects the map is
60
+ extra overhead. Full method, per-task results and caveats: [docs/benchmark](https://github.com/monkeymars/vibesequence/blob/main/docs/benchmark/README.md).
61
+
62
+ ## Quick start
63
+
64
+ ```bash
65
+ pip install vibesequence # Python projects
66
+ pip install "vibesequence[ts]" # + TypeScript / JavaScript
67
+
68
+ vibesequence build . # parse the repo into vibesequence-out/
69
+ vibesequence serve # open the viewer at http://127.0.0.1:8765
70
+ ```
71
+
72
+ Try it on the bundled examples: clone this repo and run `vibesequence build examples`.
73
+
74
+ ## Commands
75
+
76
+ | command | what it does |
77
+ |---|---|
78
+ | `vibesequence build .` | parse the repo into `vibesequence-out/` (incremental) |
79
+ | `vibesequence serve` | live viewer; source links open your editor |
80
+ | `vibesequence list` | list all flows |
81
+ | `vibesequence show "POST /orders"` | one flow as Mermaid (`--format text` for the AI-friendly tree) |
82
+ | `vibesequence callers OrderRepo.create` | who calls this, and which flows are affected |
83
+ | `vibesequence diff` | which flows changed in the last build |
84
+ | `vibesequence install` | add instructions to `CLAUDE.md` + a Claude Code hook that shows changed flows after each edit |
85
+
86
+ ## Viewer shortcuts
87
+
88
+ `Space` play · `←` `→` step · `[` `]` level · `Z` zen · `D` dark mode · `\` sidebar · `⌘`+scroll zoom · drag to pan
89
+
90
+ ## Roadmap
91
+
92
+ - [ ] **Go**: tree-sitter extractor, entry points for `net/http`, Gin, Echo, Chi and Cobra commands
93
+ - [ ] **Java**: Spring controllers
94
+ - [ ] **Smarter resolution**: return-type inference, dependency injection, interface → implementations
95
+ - [ ] **Diff against a git ref**: review a PR as sequence changes, not line changes
96
+ - [ ] **MCP server**: let the assistant call `list_flows` / `show_flow` instead of reading files
97
+ - [ ] **Runtime traces** (optional): mark the paths that actually ran
98
+
99
+ ## Contributing
100
+
101
+ Open for contributions! Pick a roadmap item, fix a bug, or improve the docs. Issues and pull requests are welcome.
102
+
103
+ - **New language**: add one extractor class in `vibesequence/extractors/` that outputs `ModuleFacts`
104
+ (see `extractors/base.py`). The resolver, viewer and CLI work without changes.
105
+ - **Set up**: clone the repo and run `pip install -e ".[ts]"`.
106
+ - **Run the tests** before opening a PR: `python tests/test_flow.py`
107
+ - Not sure where to start? Open an issue and say what you want to build.
108
+
109
+ ## Learn more
110
+
111
+ How the engine turns code into sequence diagrams (extract → resolve → expand → project → render), plus
112
+ live mode, editor links and the AI hook: [docs/how-it-works.md](https://github.com/monkeymars/vibesequence/blob/main/docs/how-it-works.md).
@@ -0,0 +1,90 @@
1
+ # vibesequence
2
+
3
+ Turn your codebase into **zoomable sequence diagrams** that show *when a user does X, who calls whom,
4
+ and in what order*. Use them as a map while you build, review or learn the code, on your own or with an
5
+ AI assistant.
6
+
7
+ Inspired by [graphify](https://github.com/Graphify-Labs/graphify), which turns a codebase into a knowledge
8
+ graph so an AI assistant can navigate it without re-reading every file. vibesequence takes the same idea
9
+ and adds what a graph leaves out: the **order** of calls and the branches around them (`if`, loops, error
10
+ handling), drawn as user sequence diagrams that you and the assistant can both read.
11
+
12
+ ![vibesequence demo](https://raw.githubusercontent.com/monkeymars/vibesequence/main/demo/vibesequence-demo.gif)
13
+
14
+ ## Why
15
+
16
+ When an AI assistant edits your code, it is hard to see what actually changed in the behavior.
17
+ vibesequence reads your code (no running it), finds the entry points (HTTP routes, CLI commands, handlers),
18
+ and draws each one as a sequence diagram:
19
+
20
+ - **Zoom** between package, module, class and function level. Click a blue call to open it on its own.
21
+ - **Play** a flow arrow by arrow, with the `file:line` of each step.
22
+ - **Live**: while the AI edits, changed steps turn green and you can play only the changes.
23
+ - **AI friendly**: the same flow is available as compact text, so your assistant reads one diagram instead of eight files.
24
+
25
+ Supports Python (stdlib only) and TypeScript / JavaScript (Next.js, Express).
26
+
27
+ ## Does it save tokens?
28
+
29
+ We benchmarked Claude Code on the same tasks with and without vibesequence. Every run passed in both setups.
30
+
31
+ | task type | input tokens | cost | turns |
32
+ |---|---|---|---|
33
+ | understanding a flow ("what happens when…", "what breaks if I change…") | **−23%** | **−18%** | 8 → 4 |
34
+ | small edits | +19% | +15% | 5.5 → 6 |
35
+
36
+ Impact questions save the most: one `vibesequence callers` call replaced 6–7 greps and reads (−53% tokens).
37
+ For edits, the agent still has to read the files it changes, so on the tiny example projects the map is
38
+ extra overhead. Full method, per-task results and caveats: [docs/benchmark](https://github.com/monkeymars/vibesequence/blob/main/docs/benchmark/README.md).
39
+
40
+ ## Quick start
41
+
42
+ ```bash
43
+ pip install vibesequence # Python projects
44
+ pip install "vibesequence[ts]" # + TypeScript / JavaScript
45
+
46
+ vibesequence build . # parse the repo into vibesequence-out/
47
+ vibesequence serve # open the viewer at http://127.0.0.1:8765
48
+ ```
49
+
50
+ Try it on the bundled examples: clone this repo and run `vibesequence build examples`.
51
+
52
+ ## Commands
53
+
54
+ | command | what it does |
55
+ |---|---|
56
+ | `vibesequence build .` | parse the repo into `vibesequence-out/` (incremental) |
57
+ | `vibesequence serve` | live viewer; source links open your editor |
58
+ | `vibesequence list` | list all flows |
59
+ | `vibesequence show "POST /orders"` | one flow as Mermaid (`--format text` for the AI-friendly tree) |
60
+ | `vibesequence callers OrderRepo.create` | who calls this, and which flows are affected |
61
+ | `vibesequence diff` | which flows changed in the last build |
62
+ | `vibesequence install` | add instructions to `CLAUDE.md` + a Claude Code hook that shows changed flows after each edit |
63
+
64
+ ## Viewer shortcuts
65
+
66
+ `Space` play · `←` `→` step · `[` `]` level · `Z` zen · `D` dark mode · `\` sidebar · `⌘`+scroll zoom · drag to pan
67
+
68
+ ## Roadmap
69
+
70
+ - [ ] **Go**: tree-sitter extractor, entry points for `net/http`, Gin, Echo, Chi and Cobra commands
71
+ - [ ] **Java**: Spring controllers
72
+ - [ ] **Smarter resolution**: return-type inference, dependency injection, interface → implementations
73
+ - [ ] **Diff against a git ref**: review a PR as sequence changes, not line changes
74
+ - [ ] **MCP server**: let the assistant call `list_flows` / `show_flow` instead of reading files
75
+ - [ ] **Runtime traces** (optional): mark the paths that actually ran
76
+
77
+ ## Contributing
78
+
79
+ Open for contributions! Pick a roadmap item, fix a bug, or improve the docs. Issues and pull requests are welcome.
80
+
81
+ - **New language**: add one extractor class in `vibesequence/extractors/` that outputs `ModuleFacts`
82
+ (see `extractors/base.py`). The resolver, viewer and CLI work without changes.
83
+ - **Set up**: clone the repo and run `pip install -e ".[ts]"`.
84
+ - **Run the tests** before opening a PR: `python tests/test_flow.py`
85
+ - Not sure where to start? Open an issue and say what you want to build.
86
+
87
+ ## Learn more
88
+
89
+ How the engine turns code into sequence diagrams (extract → resolve → expand → project → render), plus
90
+ live mode, editor links and the AI hook: [docs/how-it-works.md](https://github.com/monkeymars/vibesequence/blob/main/docs/how-it-works.md).
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"] # 77+: SPDX `license` string (PEP 639)
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vibesequence"
7
+ version = "0.1.0"
8
+ description = "Turn a codebase into zoomable user sequence diagrams, for you and your AI assistant"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Gery Wibowo" }]
13
+ requires-python = ">=3.10"
14
+ keywords = ["sequence-diagram", "code-visualization", "mermaid", "static-analysis", "ai-assistant"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "Topic :: Software Development :: Documentation",
20
+ ]
21
+ dependencies = [] # Python projects: stdlib only
22
+
23
+ [project.optional-dependencies]
24
+ ts = ["tree-sitter>=0.23", "tree-sitter-typescript>=0.23"] # TypeScript / JavaScript projects
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/monkeymars/vibesequence"
28
+ Documentation = "https://github.com/monkeymars/vibesequence/blob/main/docs/how-it-works.md"
29
+ Issues = "https://github.com/monkeymars/vibesequence/issues"
30
+
31
+ [project.scripts]
32
+ vibesequence = "vibesequence.cli:main"
33
+
34
+ [tool.setuptools.packages.find]
35
+ include = ["vibesequence*"]
36
+
37
+ [tool.setuptools.package-data]
38
+ "vibesequence.render" = ["viewer.html"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,179 @@
1
+ """Smoke check: python tests/test_flow.py (also runs under pytest)."""
2
+ import contextlib
3
+ import io
4
+ import sys
5
+ import tempfile
6
+ import textwrap
7
+ from pathlib import Path
8
+
9
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
10
+ from vibesequence import builder, cli # noqa: E402
11
+ from vibesequence.flow import expand, project # noqa: E402
12
+ from vibesequence.render.mermaid import render # noqa: E402
13
+
14
+ APP = textwrap.dedent('''
15
+ def save(x): pass
16
+ def notify(x): pass
17
+ def audit(x): pass
18
+
19
+ def handle(user, items):
20
+ if not user:
21
+ notify(user)
22
+ if user.admin:
23
+ audit(user)
24
+ for item in items:
25
+ save(item)
26
+
27
+ def main():
28
+ handle(None, [])
29
+ ''')
30
+
31
+
32
+ def _run(*argv) -> str:
33
+ buf = io.StringIO()
34
+ with contextlib.redirect_stdout(buf):
35
+ cli.main(list(argv))
36
+ return buf.getvalue()
37
+
38
+
39
+ def test_flow():
40
+ with tempfile.TemporaryDirectory() as d:
41
+ root, out = Path(d, "src"), Path(d, "out")
42
+ root.mkdir()
43
+ (root / "app.py").write_text(APP)
44
+ graph = builder.build(root, out)
45
+ again = builder.build(root, out) # live mode relies on: same code -> same version
46
+ assert not again.stats["changed"] and again.stats["version"] == graph.stats["version"]
47
+
48
+ mm = render(project(graph, expand(graph, "app:handle", 3), "function"), "function")
49
+ assert "opt if not user" in mm, mm
50
+ assert "opt if user.admin" in mm, mm # two separate ifs stay two fragments
51
+ assert "loop for item in items" in mm, mm
52
+
53
+ text = _run("show", "app:handle", "--out", str(out), "--format", "text", "--level", "function")
54
+ assert "save app.py:2" in text, text # definition location for the agent
55
+
56
+ callers = _run("callers", "app:save", "--out", str(out))
57
+ assert "app:handle app.py:12" in callers, callers
58
+ assert "main()" in callers, callers # flow that reaches it
59
+
60
+ (root / "app.py").write_text(APP.replace(" save(item)", " save(item)\n notify(item)"))
61
+ hook = _run("build", str(root), "--out", str(out), "--hook")
62
+ assert builder.load(out).stats["version"] != graph.stats["version"]
63
+ assert "+" in hook and "notify" in hook, hook
64
+ assert _run("build", str(root), "--out", str(out), "--hook") == "" # no change -> silent
65
+
66
+
67
+ def test_serve_open_guards():
68
+ """POST /__open must refuse other origins and files outside the project (never launches an editor)."""
69
+ import json
70
+ import threading
71
+ import urllib.error
72
+ import urllib.request
73
+ from http.server import ThreadingHTTPServer
74
+ from vibesequence import editor, serve
75
+
76
+ editor.EDITORS["missing"] = ("Missing", lambda f, n: ["vibesequence-no-such-editor", f])
77
+ with tempfile.TemporaryDirectory() as d:
78
+ root = Path(d)
79
+ (root / "app.py").write_text("x = 1\n")
80
+ srv = ThreadingHTTPServer(("127.0.0.1", 0), serve._handler(root, root.resolve(), lambda: None, "missing"))
81
+ threading.Thread(target=srv.serve_forever, daemon=True).start()
82
+ base = f"http://127.0.0.1:{srv.server_address[1]}"
83
+
84
+ def post(body, origin=base):
85
+ req = urllib.request.Request(base + "/__open", json.dumps(body).encode(), {"Origin": origin})
86
+ try:
87
+ return urllib.request.urlopen(req).status
88
+ except urllib.error.HTTPError as e:
89
+ return e.code
90
+
91
+ assert post({"file": "app.py", "line": 1}, "https://evil.example") == 403
92
+ assert post({"file": "../../etc/passwd", "line": 1}) == 403
93
+ assert post({"file": "app.py", "line": "x"}) == 400
94
+ assert post({"file": "app.py", "line": 1}) == 404 # CLI not on PATH
95
+ srv.shutdown()
96
+
97
+
98
+ def test_detect_editor():
99
+ from vibesequence.editor import detect
100
+ assert detect({"TERM_PROGRAM": "zed", "ZED_TERM": "true"})[0] == "zed"
101
+ assert detect({"TERM_PROGRAM": "vscode", "__CFBundleIdentifier": "com.microsoft.VSCode"})[0] == "vscode"
102
+ assert detect({"TERM_PROGRAM": "vscode", "__CFBundleIdentifier": "com.todesktop.230313mzl4w4u92"})[0] == "cursor"
103
+ assert detect({"VISUAL": "code --wait"})[0] == "vscode"
104
+ assert detect({"EDITOR": "vim", "TERM_PROGRAM": "vscode"})[0] == "vscode" # terminal editors skipped
105
+ assert detect({"TERMINAL_EMULATOR": "JetBrains-JediTerm", "__CFBundleIdentifier": "com.jetbrains.intellij"})[0] == "idea"
106
+ assert detect({"VIBESEQUENCE_EDITOR": "pycharm", "TERM_PROGRAM": "zed"})[0] == "pycharm" # explicit wins
107
+
108
+
109
+ def test_install_keeps_user_content():
110
+ """install edits the user's CLAUDE.md and Claude settings: replace only our section, add the hook once."""
111
+ import json
112
+ with tempfile.TemporaryDirectory() as d:
113
+ md, settings = Path(d, "CLAUDE.md"), Path(d, ".claude", "settings.local.json")
114
+ md.write_text("# Proj\n\nKeep tests green.\n\n## vibesequence (old)\nold text\n\n## Deploy\nmake deploy\n")
115
+ settings.parent.mkdir()
116
+ settings.write_text(json.dumps({"permissions": {"allow": ["Bash(make:*)"]}}))
117
+ for _ in range(2):
118
+ with contextlib.redirect_stdout(io.StringIO()):
119
+ cli._install(md)
120
+ cli._install_hook(settings)
121
+ text, conf = md.read_text(), json.loads(settings.read_text())
122
+ assert "Keep tests green." in text and "## Deploy" in text and "old text" not in text
123
+ assert text.count("## vibesequence") == 1
124
+ assert conf["permissions"] == {"allow": ["Bash(make:*)"]}
125
+ assert len(conf["hooks"]["PostToolUse"]) == 1
126
+
127
+
128
+ def test_import_root_and_ranking():
129
+ """Built one folder too high, imports still resolve; roots that drive more code rank first."""
130
+ with tempfile.TemporaryDirectory() as d:
131
+ pkg = Path(d, "services", "app")
132
+ pkg.mkdir(parents=True)
133
+ (pkg / "lib.py").write_text("def f(): g()\ndef g(): h()\ndef h(): pass\n")
134
+ (pkg / "main.py").write_text(textwrap.dedent('''
135
+ from app.lib import f, g, h
136
+
137
+ def run():
138
+ f()
139
+ g()
140
+ h()
141
+
142
+ def _cb():
143
+ h()
144
+ '''))
145
+ graph = builder.build(Path(d), Path(d, "out"))
146
+ assert graph.stats["import_roots"] == ["services"], graph.stats
147
+ assert "app.main:run" in graph.functions
148
+ assert graph.functions["app.main:run"].calls[0].resolved == "app.lib:f"
149
+ roots = [e.id for e in graph.entries if e.kind == "root"]
150
+ assert roots.index("app.main:run") < roots.index("app.main:_cb"), roots
151
+
152
+
153
+ def test_typescript_next_and_express():
154
+ """examples/next_shop: Next.js route files + Express routes resolve through classes, inheritance,
155
+ path aliases and an imported instance. Skipped without the optional tree-sitter dependency."""
156
+ try:
157
+ import tree_sitter_typescript # noqa: F401
158
+ except ImportError:
159
+ print("skip test_typescript_next_and_express: pip install 'vibesequence[ts]'")
160
+ return
161
+ example = Path(__file__).resolve().parents[1] / "examples" / "next_shop"
162
+ with tempfile.TemporaryDirectory() as d:
163
+ graph = builder.build(example, Path(d))
164
+ labels = {e.label for e in graph.entries if e.kind == "route"}
165
+ assert {"POST /api/orders", "GET /api/orders/[id]", "GET /api/me", "GET /orders/:id", "POST /orders/:id/cancel"} <= labels, labels
166
+ text = _run("show", "POST /api/orders", "--out", d, "--format", "text", "--level", "function", "--depth", "6")
167
+ for step in ("auth.requireUser", "OrderService.place", "BaseRepo.get", "for each in items",
168
+ "PricingEngine.priceLine", "OrderRepo.markFailed", "fetch", "pool.query", "orders/route.POST"):
169
+ assert step in text, (step, text)
170
+
171
+
172
+ if __name__ == "__main__":
173
+ test_typescript_next_and_express()
174
+ test_import_root_and_ranking()
175
+ test_flow()
176
+ test_serve_open_guards()
177
+ test_detect_editor()
178
+ test_install_keeps_user_content()
179
+ print("ok")
File without changes
@@ -0,0 +1,2 @@
1
+ from .cli import main
2
+ main()
@@ -0,0 +1,179 @@
1
+ """Build pipeline: files -> ModuleFacts (cached) -> resolve -> entry points -> Graph
2
+
3
+ Scalability notes:
4
+ * Extraction is per-file and cached by content hash in vibesequence-out/cache.json.
5
+ A rebuild on a 2000-file repo touches only the files whose hash changed.
6
+ * Resolution is a cheap dictionary chain, O(total calls).
7
+ * Flows are NOT precomputed. The viewer / CLI expands a flow lazily from the
8
+ graph, so output size is O(functions + calls), not O(entries × depth).
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ from collections import Counter, defaultdict
15
+ import sys
16
+ import time
17
+ from dataclasses import asdict
18
+ from pathlib import Path
19
+
20
+ from .entrypoints import detect
21
+ from .extractors.base import get_extractors
22
+ from .model import CallSite, FunctionDef, Graph, ModuleFacts
23
+ from .resolver import Resolver
24
+
25
+ CACHE_VERSION = 2 # bump when extractor output changes, so stale facts are reparsed
26
+ IGNORE_DIRS = {".git", "node_modules", "__pycache__", ".venv", "venv", "dist", "build",
27
+ "vibesequence-out", "graphify-out", ".mypy_cache", ".pytest_cache", "site-packages",
28
+ ".next", ".nuxt", ".svelte-kit", ".turbo", ".vercel", "coverage", ".cache", ".strapi"}
29
+ TS_EXTS = {".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"}
30
+
31
+
32
+ def _hash(p: Path) -> str:
33
+ return hashlib.sha1(p.read_bytes()).hexdigest()
34
+
35
+
36
+ def _facts_from_cache(d: dict) -> ModuleFacts:
37
+ fns = []
38
+ for f in d["functions"]:
39
+ f = dict(f)
40
+ f["calls"] = [CallSite(**c) for c in f["calls"]]
41
+ fns.append(FunctionDef(**f))
42
+ return ModuleFacts(module=d["module"], file=d["file"], functions=fns,
43
+ imports=d["imports"], instance_hints=d["instance_hints"],
44
+ bases=d.get("bases", {}), globals=d.get("globals", []), aliases=d.get("aliases", {}))
45
+
46
+
47
+ def build(root: Path, out_dir: Path, use_cache: bool = True) -> Graph:
48
+ t0 = time.time()
49
+ root = root.resolve()
50
+ out_dir.mkdir(parents=True, exist_ok=True)
51
+ cache_path = out_dir / "cache.json"
52
+ cache: dict = json.loads(cache_path.read_text()) if (use_cache and cache_path.exists()) else {}
53
+ if cache.get("__version__") != CACHE_VERSION:
54
+ cache = {}
55
+ extractors = get_extractors()
56
+
57
+ modules: list[ModuleFacts] = []
58
+ new_cache: dict = {"__version__": CACHE_VERSION}
59
+ files_seen = files_parsed = ts_skipped = 0
60
+ for p in sorted(root.rglob("*")):
61
+ if not p.is_file() or p.name.endswith(".d.ts"):
62
+ continue
63
+ if p.suffix not in extractors:
64
+ ts_skipped += p.suffix in TS_EXTS and not any(part in IGNORE_DIRS for part in p.relative_to(root).parts)
65
+ continue
66
+ if any(part in IGNORE_DIRS for part in p.relative_to(root).parts):
67
+ continue
68
+ files_seen += 1
69
+ rel = str(p.relative_to(root))
70
+ h = _hash(p)
71
+ if rel in cache and cache[rel]["hash"] == h:
72
+ facts = _facts_from_cache(cache[rel]["facts"])
73
+ else:
74
+ try:
75
+ facts = extractors[p.suffix].extract(p, root)
76
+ except SyntaxError as e:
77
+ print(f" skip {rel}: {e.msg} (line {e.lineno})", file=sys.stderr)
78
+ continue
79
+ files_parsed += 1
80
+ for fn in facts.functions: # reset resolution; it depends on other files
81
+ for c in fn.calls:
82
+ c.resolved = None
83
+ modules.append(facts)
84
+ new_cache[rel] = {"hash": h, "facts": asdict(facts)}
85
+
86
+ import_roots, import_warning = _apply_import_roots(modules)
87
+ resolver = Resolver(modules)
88
+ res_stats = resolver.resolve_all()
89
+ entries = detect(resolver.functions)
90
+
91
+ stats = {
92
+ "root": str(root), "files": files_seen, "files_reparsed": files_parsed,
93
+ "functions": len(resolver.functions), "entries": len(entries),
94
+ **res_stats, "seconds": round(time.time() - t0, 3),
95
+ "import_roots": import_roots, "import_warning": import_warning or (
96
+ f"{ts_skipped} TypeScript/JavaScript files skipped: pip install 'vibesequence[ts]'" if ts_skipped else ""),
97
+ }
98
+ graph = Graph(functions=resolver.functions, entries=entries, stats=stats)
99
+ cache_path.write_text(json.dumps(new_cache))
100
+ graph_path = out_dir / "graph.json"
101
+ shape = _shape(graph.to_json())
102
+ old = _shape(json.loads(graph_path.read_text())) if graph_path.exists() else None
103
+ # version: changes only when functions/calls/entries change (not timings), so a live viewer
104
+ # and `serve` can tell "the flows changed" from "someone ran build again"
105
+ stats["version"] = hashlib.sha1(json.dumps(shape, sort_keys=True).encode()).hexdigest()[:12]
106
+ stats["changed"] = shape != old
107
+ # keep the graph from before the last CHANGE (not the last build) for `vibesequence diff`;
108
+ # a no-op rebuild must not overwrite it
109
+ if old is not None and stats["changed"]:
110
+ graph_path.replace(out_dir / "graph.prev.json")
111
+ graph_path.write_text(json.dumps(graph.to_json(), indent=1))
112
+ return graph
113
+
114
+
115
+ def _apply_import_roots(modules: list[ModuleFacts]) -> tuple[list[str], str]:
116
+ """Built one level too high (monorepo, `vibesequence build .` above the project)? Then imports say
117
+ `app.repos.base` while the file's module is `services.app.repos.base`, and nothing resolves.
118
+ Find such prefixes (>= 3 imports match one folder unambiguously) and strip them from module
119
+ names, as if vibesequence had been run inside that folder. Skipped, with a warning, when stripping
120
+ would collide with an existing module or the match is ambiguous."""
121
+ known = {m.module for m in modules}
122
+ prefixes_of: dict[str, set[str]] = defaultdict(set) # "app.repos.base" -> {"services"}
123
+ for name in known:
124
+ parts = name.split(".")
125
+ for i in range(1, len(parts)):
126
+ prefixes_of[".".join(parts[i:])].add(".".join(parts[:i]))
127
+ hits: Counter[str] = Counter()
128
+ ambiguous: Counter[str] = Counter()
129
+ for m in modules:
130
+ for target in m.imports.values():
131
+ candidates = [t for t in (target, target.rpartition(".")[0]) if t]
132
+ if any(t in known for t in candidates):
133
+ continue
134
+ for t in candidates:
135
+ if len(prefixes_of.get(t, ())) == 1:
136
+ hits[next(iter(prefixes_of[t]))] += 1
137
+ break
138
+ if prefixes_of.get(t):
139
+ ambiguous[" / ".join(sorted(prefixes_of[t]))] += 1
140
+ break
141
+ applied, warning = [], ""
142
+ for prefix, n in hits.most_common():
143
+ if n < 3:
144
+ break
145
+ dot = prefix + "."
146
+ renamed = {m.module: m.module[len(dot):] for m in modules if m.module.startswith(dot)}
147
+ if set(renamed.values()) & (known - set(renamed)) or len(set(renamed.values())) < len(renamed):
148
+ warning = f"imports match {prefix.replace('.', '/')}/ but stripping it collides with other modules: build that folder"
149
+ continue
150
+ cut = lambda s: s[len(dot):] if s.startswith(dot) else s # noqa: E731
151
+ for m in modules:
152
+ m.module = cut(m.module)
153
+ m.imports = {k: cut(v) for k, v in m.imports.items()}
154
+ for fn in m.functions:
155
+ fn.module = cut(fn.module)
156
+ fn.id = f"{fn.module}:{fn.id.partition(':')[2]}"
157
+ known = {m.module for m in modules}
158
+ applied.append(prefix.replace(".", "/"))
159
+ if not applied and sum(ambiguous.values()) >= 3:
160
+ where = ambiguous.most_common(1)[0][0].replace(".", "/")
161
+ warning = f"imports match several folders ({where}): build one project at a time"
162
+ return applied, warning
163
+
164
+
165
+ def _shape(d: dict) -> tuple:
166
+ # ponytail: line shifts count as a change, so a comment-only edit rotates graph.prev.json and
167
+ # `vibesequence diff` then says "no flow changes"; compare line-free shapes if that bites
168
+ return d["functions"], d["entries"] # everything but stats (timings)
169
+
170
+
171
+ def load(out_dir: Path, name: str = "graph.json") -> Graph:
172
+ d = json.loads((out_dir / name).read_text())
173
+ fns = {}
174
+ for k, f in d["functions"].items():
175
+ f = dict(f)
176
+ f["calls"] = [CallSite(**c) for c in f["calls"]]
177
+ fns[k] = FunctionDef(**f)
178
+ from .model import EntryPoint
179
+ return Graph(functions=fns, entries=[EntryPoint(**e) for e in d["entries"]], stats=d["stats"])