Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ dist/
build/
*.egg-info/
__pycache__/
memory/
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@ talk to Redis, OpenAI, etc. Large repos (350+ files) are automatically aggregate
directory-level nodes so the graph stays readable; override with `--graph-detail files` or
`--graph-detail dirs`.

Test files (`tests/`, `test_*.py`, `conftest.py`, `*.spec.ts`, `*_test.go`, …) get their own
**Test Graph** section below the main one, showing which modules and services each test reaches.
Fixtures and test doubles name every service they exercise, so keeping them in the main graph
buries the real dependency structure under service edges.

Both sections have a **Flow | Graph** toggle over the same nodes and edges. **Flow** (the default
up to 80 nodes) is a left-to-right layered diagram: columns are dependency depth from the sources
(HTTP entry and files nothing imports), every detected service is its own node in the last column,
and import cycles are drawn dashed. **Graph** is the force-directed view. Moving dots show data
going both ways: a filled dot is the call/request, a hollow dot the return/response. On graphs
with more than 60 edges, only the paths of the clicked or hovered node animate. Search, "hide
isolated" and the selected node are shared between the two views.

The older layered **Full System Architecture** diagram is collapsed by default, since the
Architecture Map covers the same ground.

By default the graph only shows what the *code* says (import direction, static "this file calls
Redis"), which is honest but not the same as real traffic. Pass `--access-log /path/to/access.log`
(any combined/common log format) to overlay real observed request counts onto the HTTP-entry
Expand Down
40 changes: 36 additions & 4 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,19 @@ Scan the current project and produce a complete visual system workflow with conc

2. **Run the analyzer**:
```bash
python3 ~/.claude/skills/workflow-generator/scripts/analyze.py <project_root> <project_root>/WORKFLOW.html
python3 ~/.claude/skills/workflow-generator/scripts/analyze.py <project_root> <project_root>/WORKFLOW.html --enrich-labels
```

Optional flags, appended after the two positional args:
- `--access-log <path>` — overlay real request counts (from a combined/common log file) onto
the codebase dependency graph's HTTP-entry edges, instead of import-direction only.
- `--graph-detail auto|files|dirs` — force file-level or directory-level graph nodes. Default
`auto` aggregates to directories once a project exceeds ~350 source files.
- `--enrich-labels` — always pass this when running via this skill (you're already an
authenticated `claude` session, so the cost is already accepted). It relabels
Architecture Map service nodes by having a local `claude -p` call actually read each
file, instead of the mechanical stem-dictionary label (see Architecture Map below).
It's a no-op everywhere else if `claude` isn't on PATH — never fails the scan.

3. **Open the output**:
```bash
Expand Down Expand Up @@ -119,16 +124,43 @@ The generated `WORKFLOW.html` always contains all of the following:
from the same min() comparison used for the "Practical Throughput" stat, so this
section and that stat can never disagree with each other.

6. **Codebase Dependency Graph** — a force-directed graph of every source file (Python, JS/TS,
6. **Architecture Map** — a curated, high-level view of *how the system works*, distinct from the
raw dependency graph below it: one node per entry point (grouped by router/controller file, not
by URL prefix), internal service, datastore, external integration, scheduled job, and — when
detected — AI agent/model/tool. Built entirely from already-detected facts plus a few light,
deterministic file-pattern checks (composition-root file, `crud`/`service`/`repository`-named
modules, tool-binding/agent-framework signatures); by default no LLM judgment is involved, so
the node/edge set is identical across the CLI, MCP server, Copilot extension, and this skill.
Service-node **labels** are the one part of this that stays mechanical by default (a fixed
stem→label dictionary, so a generically-named file gets no node at all) — pass
`--enrich-labels` (always on when invoked via this skill; opt-in flag/param on CLI, MCP's
`generate_workflow` tool, and the Copilot extension's `enrichLabels` request field) to have a
local `claude -p` call read each service file and relabel it with what the code actually does.
It only ever renames existing nodes — never invents new ones — and silently no-ops if `claude`
isn't installed, so the deterministic output is always the floor. Entry→service edges are only
drawn when the entry file actually references the service module's name — never a blanket
fan-out. Click any node for a detail panel (what it is, its source file); click **Show import
details** to jump to the Codebase Dependency Graph below with that component's exact files
isolated and highlighted. Nodes sharing a feature (inferred from router/service naming) cluster
together with a matching accent color and group label.

7. **Codebase Dependency Graph** — a force-directed graph of every source file (Python, JS/TS,
Go, Java, Rust, Ruby) as a node and every real, parser-resolved import as an edge; files that
match an already-detected component (an LLM call, a database client, a queue) also get an edge
to that component. Click a node to isolate its neighbors; hover for file details. Large
projects (350+ files) are aggregated to directory-level nodes automatically. Edges are
import-direction only (static, honest) unless `--access-log` was supplied, in which case
HTTP-entry edges are weighted with real observed request counts — the graph is explicit about
which kind of edge is which so it's never mistaken for a traced request path.

7. **Guided tour** — a spotlight walkthrough that dims the page and steps through each section
Test files are split into a separate **Test Graph** section (tests plus the modules/services
they import or call), so they don't clutter the main graph.
Each has a **Flow | Graph** toggle over the same nodes/edges: Flow is a layered left-to-right
diagram (depth columns, every service its own node in the last column, cycles dashed), Graph
the force-directed view. Dots animate data both ways (filled = call, hollow = return); past 60
edges only the clicked/hovered node's paths animate. The Full System Architecture section is
collapsed by default.

8. **Guided tour** — a spotlight walkthrough that dims the page and steps through each section
in turn (Back/Next/Skip), shown automatically the first time a generated report is opened.
Steps for sections that don't apply to a given project (e.g. no detected routes, no graph)
are skipped automatically. Replayable anytime via the `?` button next to the theme toggle.
Expand Down
4 changes: 3 additions & 1 deletion copilot/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ app.post("/agent", async (req, res) => {
res.write(`data: {"choices":[{"delta":{"content":"Scanning project at \`${projectDir}\`...\\n\\n"}}]}\n\n`);

try {
const out = execFileSync("python3", [ANALYZE_PY, projectDir, outputFile], { encoding: "utf8" });
const args = [ANALYZE_PY, projectDir, outputFile];
if (req.body.enrichLabels) args.push("--enrich-labels");
const out = execFileSync("python3", args, { encoding: "utf8" });
const lines = out.trim().split("\n").map(l => `> ${l}`).join("\n");
res.write(`data: {"choices":[{"delta":{"content":"${lines.replace(/"/g,'\\"').replace(/\n/g,'\\n')}\\n\\nWorkflow diagram written to \`${outputFile}\`"}}]}\n\n`);
} catch (err) {
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "workflow-generator-mcp"
version = "0.3.1"
version = "0.5.0"
description = "Generate a visual system architecture diagram (WORKFLOW.html) with concurrency capacity estimates and bottleneck analysis from any codebase — CLI + MCP server, stdlib-only scanner."
readme = "README.md"
license = { text = "MIT" }
Expand Down
4 changes: 2 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
"url": "https://github.com/askuma/workflow-generator",
"source": "github"
},
"version": "0.3.1",
"version": "0.4.0",
"packages": [
{
"registryType": "pypi",
"identifier": "workflow-generator-mcp",
"version": "0.3.1",
"version": "0.4.0",
"transport": {
"type": "stdio"
}
Expand Down
53 changes: 53 additions & 0 deletions tests/test_graph_tests_split.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from conftest import write_project

ANALYSIS = {
'llm': {'providers': ['OpenAI']}, 'storage': [], 'queues': [], 'external_sources': [],
'framework': None, 'access_log': None,
}


def test_is_test_path(az_module):
az = az_module
for rel in ('tests/helpers.py', 'pkg/test_x.py', 'pkg/x_test.go', 'conftest.py',
'web/a.spec.ts', 'web/a.test.jsx', 'src/test/java/FooTest.java', 'lib/a_spec.rb'):
assert az._is_test_path(rel), rel
for rel in ('app/main.py', 'pkg/testing_utils.py', 'src/latest.ts', 'Manifest.java'):
assert not az._is_test_path(rel), rel


def test_tests_split_into_their_own_graph(az_module, tmp_path):
az = az_module
root = write_project(tmp_path, {
"app.py": "import util\n",
"util.py": "",
"tests/test_app.py": "import app\nfrom openai import OpenAI\nOpenAI().chat.completions.create()\n",
})
main, tests = az.build_code_graphs(root, ANALYSIS, detail='files')
main_ids = {n['id'] for n in main['nodes']}
test_ids = {n['id'] for n in tests['nodes']}

assert main_ids == {'app.py', 'util.py'}
assert main['meta']['files'] == 2
# A service only a test reaches must not leak into the main graph.
assert 'svc:OpenAI' not in main_ids

assert test_ids == {'tests/test_app.py', 'app.py', 'svc:OpenAI'}
assert tests['meta']['files'] == 1
assert all(e['s'] == 'tests/test_app.py' for e in tests['links'])


def test_render_codebase_sections_have_flow_and_graph_views(az_module, tmp_path):
az = az_module
root = write_project(tmp_path, {
"app.py": "import util\n",
"util.py": "",
"tests/test_app.py": "import app\n",
})
html = az.render_html(az.collect(root), "p")
assert 'id="section-flow"' not in html # merged into the Codebase section
for P in ('graph-', 'test-graph-'):
for suffix in ('tab-flow', 'tab-force', 'flow-canvas', 'canvas', 'data', 'animate'):
assert f'id="{P}{suffix}"' in html, P + suffix
assert html.index('id="section-graph"') < html.index('id="section-test-graph"')
# Small graphs open on the Flow view.
assert 'id="section-graph" data-default-view="flow"' in html
Loading
Loading