Spaghetti-code and architectural-smell detector for Python projects
Project description
spaghetti
Spaghetti-code and architectural-smell detector for the Boti workspace.
Scans workspace packages for anti-patterns, architectural violations, and structural code smells — from single-function issues (long functions, deep nesting, high cyclomatic complexity) up to whole-package issues that only show up once you can see across files: real circular imports (not just a parent/child heuristic), copy-pasted function bodies, and the sync/async "twin" duplication pattern (load/aload, foo/foo_async) where a fix applied to one twin silently never reaches the other.
Every requested package is reviewed concurrently — one boti.core.Agent (SpaghettiReviewAgent) per package running its scan via asyncio.to_thread — then folded into a single consolidated report.
spaghetti is not tied to the Boti workspace: the package registry it scans is generic and configurable via a YAML file, ad-hoc CLI flags, or both — see Configuring packages.
Usage
uv run spaghetti
uv run spaghetti --packages boti-data boti-dask
uv run spaghetti --severity error
uv run spaghetti --top 10 --exclude tests/ examples/
uv run spaghetti --json > report.json
uv run spaghetti --plan --top 10
uv run spaghetti --config spaghetti.yaml
uv run spaghetti --package my-lib=my-lib/src/my_lib
Exit codes: 0 (clean), 1 (warnings present), 2 (errors present) — safe to wire into CI as a gate.
Options
| Flag | Default | Description |
|---|---|---|
--config |
none | YAML file with a packages: {name: path} mapping (see below); replaces the built-in defaults |
--package |
none | Add or override one package as NAME=PATH (repeatable); applied on top of --config or the defaults |
--packages |
all resolved packages | Names to scan from the resolved registry |
--severity |
info |
Minimum severity to display (info / warning / error) |
--json |
off | Output as JSON instead of the console report |
--top |
5 |
Number of worst files to list per package |
--exclude |
none | Path substrings to exclude from scanning |
--min-duplicate-lines |
5 |
Minimum function length to consider for duplicate-body detection |
--twin-similarity |
0.6 |
Minimum text-similarity ratio (0–1) to flag a sync/async twin pair |
--plan |
off | Output a prioritized remediation plan instead of the standard report |
Run uv run spaghetti --help for the full list.
Inline Suppression
Suppress specific findings on a line with # spaghetti-ignore[rule]:
# Suppress a specific rule
def f(): # spaghetti-ignore[long-function]: intentionally large
...
# Suppress all rules on a line
x: dict = {} # spaghetti-ignore: reviewed, no issue
The marker applies to the line it appears on and the line directly above it (so a marker can sit above a def line too long for a trailing comment). Suppressed findings are counted in the report (suppressed: N in the header) rather than silently dropped — they remain visible.
JSON Output
With --json, the report is a single JSON object to stdout:
{
"issues": [
{
"file": "src/my_module.py",
"line": 42,
"severity": "warning",
"rule": "long-function",
"message": "my_func() is 65 lines (max 50)",
"package": "my-lib"
}
],
"suppressed": 3
}
Remediation Plan
With --plan, the detector outputs a prioritized fix order instead of the standard report. Each rule is scored by severity_weight × fix_effort and grouped into priority levels (P0–P3):
uv run spaghetti --plan --top 10
Priority levels:
- P0 (score ≥ 12): CRITICAL — fix immediately (e.g., circular imports, god-classes)
- P1 (score ≥ 7): HIGH — fix this sprint
- P2 (score ≥ 3): MEDIUM — plan for next cycle
- P3 (score < 3): LOW — track in backlog
The plan groups issues by rule, counts affected files, and lists a recommended fix order. This makes it easy to start a code-quality improvement cycle with the highest-impact fixes first.
Rules
The detector checks 36 rules across four tiers:
Per-file AST checks (30 rules): long-function, high-complexity, missing-return-type, missing-param-type, too-many-params, excessive-returns, boolean-flag-params, deep-nesting, untyped-dict, unused-import, swallowed-exception, duplicate-branch, encapsulation-violation, god-class, layer-violation, transport-in-library, potential-circular-import, god-module, mutable-default, bare-except, star-import, global-mutable, scope-mutation, dead-code, message-chain, excessive-decorators, magic-number, missing-else, lazy-class, deep-inheritance.
Per-file source-text checks (2 rules): long-file, todo-marker.
Infrastructure checks (1 rule): syntax-error (files that fail ast.parse()).
Per-package cross-file checks (3 rules): import-cycle, duplicate-function-body, sync-async-duplication.
See SDD.md for the full rule catalog, thresholds, and scoring formula.
Configuring packages
With no flags, spaghetti scans this workspace's own DEFAULT_PACKAGES (boti, boti-data, boti-dask; see src/spaghetti/detector.py). To point it at other packages — in this workspace, another workspace, or any directory on disk — use --config and/or --package.
Precedence:
- Neither flag given → the built-in defaults are used as-is.
--configgiven → itspackages:mapping replaces the defaults entirely, so a config file states the full set explicitly rather than silently inheriting unrelated hardcoded packages.--package NAME=PATHentries are then overlaid on top of whichever set (1) or (2) produced — adding new names or overriding ones already defined, so a config file plus a quick ad-hoc addition both work together.
--config: YAML file
# spaghetti.yaml
packages:
my-lib: my-lib/src/my_lib
my-service: services/my-service/src/my_service
Paths resolve relative to the config file's own directory, not the caller's working directory, so the same config works no matter where you invoke spaghetti from.
uv run spaghetti --config spaghetti.yaml
--package: ad-hoc CLI entries
uv run spaghetti --package my-lib=my-lib/src/my_lib --package other=../other/src/other
Repeatable; paths resolve relative to the current directory. Combine with --config to override or extend a config file for one run without editing it.
Development
uv run pytest spaghetti/tests/
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file spaghetti_detector-0.1.0.tar.gz.
File metadata
- Download URL: spaghetti_detector-0.1.0.tar.gz
- Upload date:
- Size: 40.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fd800fd4fe6a40c74eec0df7fbe524171da9a6d702a8eb3fe4e8c3313902be4b
|
|
| MD5 |
8c9fe379af7eeb7851f21d6fb5269412
|
|
| BLAKE2b-256 |
817f9c4e394d1af3f1f2457987c2ac0cc2bdc864bac60ccef3f708a233b7748e
|
File details
Details for the file spaghetti_detector-0.1.0-py3-none-any.whl.
File metadata
- Download URL: spaghetti_detector-0.1.0-py3-none-any.whl
- Upload date:
- Size: 33.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1ab7808948fe8cdd5fe941ba1fcc32486d3c6b5e3c482d23c7cdfbe42a323911
|
|
| MD5 |
e95f14f722a7f0108016e3ce1a7b9255
|
|
| BLAKE2b-256 |
565e12f8abbf76081ec198ccab19e1d13dafa43e2a3e8c7c75dc9320aa8e5f66
|