Static structural graph tool for JavaScript / TypeScript and Frappe / Python repositories. Points at a repo, produces a structural flow graph (graph.json) and a local HTML view. v1.1.0 — Phases 0–7 complete + incremental scan cache.
The interactive feature viewer (
forge-sutra viewer) on a live echo-ai scan — 643 nodes, 1,206 edges, 32 features. Health-scored feature cards, search, health/confidence filters, and Overview / Ecosystem tabs. Click any card for its drill-down (traced flows, nodes, endpoints, issues).
See your product as features — derived from code, not from docs, and honest about what it doesn't know.
- Truthful by default. Every finding is labelled candidate until it's actually confirmed (cross-repo, dynamic routes, and external hosts resolved). No "finds all bugs", no overstated claims — structural/contract drift only.
- Features, not files. The unit is a feature: what it is, what it wires to, whether the wiring is intact, and where it's broken — with a health score you can click into.
- A realistic feature viewer. An interactive local viewer — feature cards, request-flow drill-down, a cross-repo map, and live refresh on code change — so you learn something true-and-new about your codebase in seconds.
Full plan (4 epics, 23 stories, sequencing, and Definition of Done) lives here:
_bmad/ROADMAP.md
· browse every story in the plan index.
- Epic 1 — Truthful Graph: external-host allowlist, dynamic-route matcher, confidence model, cross-repo linking, incremental scan, scan diff.
- Epic 2 — Real Features: feature contracts, client↔server reconciliation, AI inference, health score, flow tracing, test-coverage mapping.
- Epic 3 — Realistic Feature Viewer ⭐: viewer app, feature cards, drill-down, cross-repo map, live/watch, search & share.
- Epic 4 — Ecosystem & SDK: language-agnostic core, Python/Frappe extractor, Forge SDK extraction, CI integration, graph history.
Try it: forge-sutra scan /path/to/repo && forge-sutra view — then watch the roadmap as the static view grows into the full feature viewer. Issues and PRs welcome.
Primary npm bin: forge-sutra. Optional alias: sutra (same entry point).
npm install -g forge-sutra
# or from source:
npm run build && npm linkAll commands work via forge-sutra or sutra. Examples below use forge-sutra.
Scans repoPath (defaults to current working directory). Resolves to an absolute path.
Produces .sutra/graph.json in the current working directory. Re-scans use a content-hashed per-file cache at .sutra/cache/ (git-ignored); delete it to force a full re-parse. The scan summary prints N cached · M parsed when the cache is active.
forge-sutra scan /path/to/my-repo
forge-sutra scan # uses cwd
sutra scan # alias — same command
Options:
--watch— debounced re-scan on TS/JS file changes (CLI-only; for live viewer usewatchcommand). Snapshots previous graph to.sutra/graph.prev.json, writes.sutra/diff.json, prints delta summary. Ctrl+C to stop. Static scan only — not a runtime monitor.--profile— print phase timings (walk, parse, checks, write) to stderr. Candidate timings only — environment-dependent, not an SLA.--ai— opt-in LLM feature naming (ai_name/ai_summaryon features). RequiresSUTRA_AI_API_KEY. Offline/no-key scans fall back to heuristic labels.--check— compare scan to baseline graph; exit 1 if new error-severity issues vs baseline (drift gate, not absolute state). Exit 2 if baseline missing or graph version mismatch.--baseline <path>— baseline for--check(default:.sutra/baseline.json).--fail-on <severity>— gate threshold:error(default),warn, orinfo.--format json— machine-readable gate output (with--check).--pr-comment [path]— Markdown PR comment body for--check(stdout or file).
Runs a full scan and writes .sutra/baseline.json for CI gating. Commit this file to pin acceptable structural state.
forge-sutra baseline
forge-sutra scan --check # fails only on new issues vs baseline
GitHub Actions (canonical):
- run: npm ci && npm run build
- run: npx forge-sutra baseline ./your-repo # once, commit .sutra/baseline.json
- run: npx forge-sutra scan ./your-repo --check
# exit 1 = new structural regression · exit 2 = missing/incompatible baselineExit codes for --check: 0 pass · 1 new issues at --fail-on threshold · 2 configuration error (no baseline or version skew).
What it does:
- Walks the repo (skips
node_modules,dist,.next,.git,coverage,out). - Parses every
.ts,.tsx,.js,.jsxfile with ts-morph (AST-based, never regex). - Emits nodes for: modules, endpoints (Next.js App Router + pages/api + Express), components, functions, handlers, tests.
- Emits edges for: imports, calls, renders (JSX), tests (test-file → subject), http (fetch/axios).
- Parses
feature.sutra.mdcontract files when present. - Runs structural + contract checks (see below).
- Groups nodes into heuristic features by directory prefix.
- Writes
.sutra/graph.json. - Prints a one-screen summary to stdout.
Reads .sutra/graph.json written by scan. Produces .sutra/view.html (a self-contained HTML document with Mermaid diagrams and issue lists). If .sutra/diff.json exists, shows a "Changes since last scan" panel. Opens it in the default browser on macOS; prints the file path on other platforms.
forge-sutra view
Starts a local HTTP server (default port 4577, binds 127.0.0.1 only) serving an interactive SPA that reads .sutra/graph.json on each request. Reload the browser tab or click Reload graph to pick up a fresh scan — no HTML rebuild required. No auth, no multi-user, localhost-only.
forge-sutra viewer
forge-sutra viewer --port 4600
The static forge-sutra view command remains the offline/no-server fallback.
Live mode: starts the viewer SPA, runs an initial scan, then re-scans on file changes (debounced) and pushes the updated graph to the browser via Server-Sent Events. Binds 127.0.0.1 only. Uses incremental scan cache when available.
forge-sutra watch
forge-sutra watch --port 4600
The viewer SPA supports substring search, health-band toggles, confidence threshold (elements without confidence show only at threshold 0), and issue-kind filters. Share this view encodes filter state in the URL hash; Export view writes a self-contained .sutra/view.<slug>.html via POST /export-view.
Diff two graph.json files. Defaults: .sutra/graph.json vs .sutra/graph.prev.json.
forge-sutra diff
forge-sutra diff .sutra/graph.json .sutra/graph.prev.json
forge-sutra diff graph-a.json graph-b.json --out .sutra/diff.json
Output: structured JSON (nodes/edges/issues added/removed/changed) + human counts line. Structural delta only — does not explain why code changed.
Emit candidate test stubs from graph issues into .sutra/scaffold/. Each file has a // CANDIDATE — generated by Sutra, not run automatically banner. Never overwrites existing files without --force.
forge-sutra scaffold
forge-sutra scaffold --from-issues orphaned_endpoint
forge-sutra scaffold --from-issues contract_missing_route --force
Stubs may not compile. Not run in CI. Not auto-test generation.
Match client graph HTTP calls against server graph routes. Emits cross_repo_orphan (warn) for client calls with no matching server route.
forge-sutra reconcile --client .sutra/all/echo-ai.json --server .sutra/all/brain-api.json
Cross-repo static match only — ignores auth, env-specific URLs, proxy rewrites, runtime 404s. Results are candidates for human review. Known proxy paths (e.g. echo-ai → brain-api via next.config rewrites) may still require manual verification.
Migrate a saved graph.json to the current schema version. Default: .sutra/graph.json.
forge-sutra migrate
forge-sutra migrate .sutra/graph.json
Migrates structure only — does not re-scan or fix semantic issues.
Version history:
| Version | Change |
|---|---|
| 0 | Phase 0 schema (no contracts field) |
| 1 | Added contracts[] from feature.sutra.md |
| 2 | Optional confidence (0..1) and provenance on nodes, edges, issues |
When GRAPH_VERSION bumps, run forge-sutra migrate on cached graphs before diffing or viewing.
{
"id": "src/api/route.ts#GET /api/foo", // stable deterministic: relPath#symbol
"type": "route|handler|component|test|endpoint|module|function",
"name": "GET /api/foo",
"file": "src/api/route.ts", // repo-relative POSIX path
"line": 12,
"data_shape": "{ id: string }", // first param type text, or null
"feature": "api", // heuristic grouping id
"confidence": 0.9, // optional 0..1; absent = unknown
"provenance": "ast-exact" // optional; see Provenance below
}Provenance values: ast-exact (AST-resolved, no guessing), heuristic (directory/name inference), template-prefix (truncated template literal URL), ai-inferred (LLM-produced, never asserted as fact).
{
"from": "src/components/Foo.tsx",
"to": "http:POST /api/bar", // or a node id, or "ext:react"
"kind": "calls|imports|renders|tests|http",
"confidence": 0.9, // optional
"provenance": "ast-exact" // optional
}{
"severity": "error|warn|info",
"kind": "orphaned_endpoint|missing_handler|dangling_test_ref|contract_parse_error|contract_missing_route|contract_undeclared_route|cross_repo_orphan",
"node": "POST /api/bar", // the thing in question
"feature": "components", // heuristic feature tag
"message": "Client calls POST /api/bar but no route handler defines it.",
"confidence": 0.4, // optional
"provenance": "template-prefix" // optional
}{
"id": "components",
"label": "Components",
"node_ids": ["..."],
"issue_count": 3,
"health": {
"score": 72,
"band": "amber",
"inputs": [ ... ],
"available_signals": ["issue_load", "orphan_ratio"]
},
"label_source": "heuristic", // or "ai-inferred" when scan --ai succeeds
"ai_name": "Authentication", // optional; display only when ai-inferred
"ai_summary": "Login and OTP." // optional; one line, length-capped
}Each feature carries a heuristic structural health score (0–100) with band green / amber / red. This is derived from code structure (issue load, orphan ratio, and optional signals when available) — not runtime correctness or test pass/fail. The viewer labels this explicitly so it is never read as a bug-free guarantee.
{
"id": "flow:app/widget/page.tsx#WidgetPage",
"entry": "app/widget/page.tsx#WidgetPage",
"steps": [
{ "node": "app/widget/page.tsx#WidgetPage", "edge": null },
{ "node": "components/WidgetButton.tsx#WidgetButton", "edge": { "from": "...", "to": "...", "kind": "renders" } }
],
"terminal": "db|handler|external|unresolved|truncated",
"confidence": "confirmed|candidate"
}Request flows are code-derived paths from entry (route/component) through renders/calls/http hops. Unresolved or dynamic-segment http targets are labelled candidate, not confirmed.
| Kind | What it catches |
|---|---|
orphaned_endpoint |
A fetch/axios call targets a METHOD+path that no endpoint node covers. |
missing_handler |
An imports/calls/renders edge references a local symbol or file that has no node in the graph. |
dangling_test_ref |
A test file imports a module that no longer exists in the repo. |
contract_parse_error |
feature.sutra.md could not be parsed (warn, non-fatal). |
contract_missing_route |
Declared endpoint in contract has no matching route in graph (error). |
contract_undeclared_route |
Route exists but not declared in contract when contract file present (warn). |
cross_repo_orphan |
Client graph HTTP call has no matching route in server graph (warn, via reconcile). |
untested_feature |
No confirmed tests edge resolves into the feature (info). Static presence only — not runtime/line coverage. |
Sutra is a static, heuristic approximation. Read this before acting on any finding.
- Structural / contract mistakes only. Sutra finds missing routes, dead imports, orphaned fetch calls, and contract drift. It does NOT find logic bugs, runtime errors, security issues, or performance problems.
- Candidate results, not complete. Dynamic imports, aliased imports, runtime-generated routes, and unresolved template-literal concatenation may produce false positives or misses.
- Static approximation. No code is executed. No type inference beyond what ts-morph surfaces.
- Not auto-debug / auto-test. Sutra does not fix code, run tests, or validate runtime behavior. Scaffold output is candidate stubs only.
- Test linkage is static, not coverage.
test_edge_count/testedcount confirmedtestsedges in the graph. They do NOT measure line coverage, branch coverage, or test pass/fail.
Known limitations:
- External hosts: Known external API hosts (Telegram, Stripe, etc.) are suppressed via allowlist + optional
.sutra/external-hosts.json. Unknown external hosts may still false-positive. - Proxy rewrites: Client-side
next.configrewrites are detected as PROXY nodes and suppress localorphaned_endpoint. Cross-repo reconcile does not automatically map proxy prefixes to server routes — verify manually. - Dynamic routes: Template-literal fetches are matched against
[param]/:paramroutes structurally. Method correctness, auth, and param types are not validated. - Contracts:
feature.sutra.mdis author-declared intent, not ground truth. Undeclared routes are warn-only when a contract file exists. - CSS/asset imports: Non-JS/TS import targets are ignored in
missing_handlerchecks. - Express variable mounts: Routers mounted via variable (e.g.,
app.use(prefix, router)) may not resolve the full path correctly. - Frappe / Python (Epic 4.2):
@frappe.whitelist()→ endpoints, DocType controllers → handlers,hooks.pydoc_events/scheduler_events→ edges. Same candidate/heuristic standard as TS. Dynamically-built dotted paths,frappe.get_attr()/frappe.call(), and runtime-overridden hooks are not confirmed.

{ "version": 5, // GRAPH_VERSION constant; bump = breaking change "repo": "my-repo", // basename of the scanned directory "scanned_at": "...", // ISO 8601 UTC timestamp "commit": "abc1234", // short git hash, or "unknown" "nodes": [SutraNode], "edges": [SutraEdge], "issues": [SutraIssue], "features": [SutraFeature], "contracts": [SutraContract], // from feature.sutra.md when present "flows": [SutraFlow] // ordered request paths (Story 2.5) }