unskein 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 (91) hide show
  1. unskein-0.1.0/.env.example +10 -0
  2. unskein-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +36 -0
  3. unskein-0.1.0/.github/ISSUE_TEMPLATE/config.yml +1 -0
  4. unskein-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +19 -0
  5. unskein-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +12 -0
  6. unskein-0.1.0/.github/workflows/ci.yml +36 -0
  7. unskein-0.1.0/.github/workflows/release.yml +33 -0
  8. unskein-0.1.0/.gitignore +40 -0
  9. unskein-0.1.0/.python-version +1 -0
  10. unskein-0.1.0/.unskein.toml.example +19 -0
  11. unskein-0.1.0/CHANGELOG.md +111 -0
  12. unskein-0.1.0/CLAUDE.md +382 -0
  13. unskein-0.1.0/CODE_OF_CONDUCT.md +21 -0
  14. unskein-0.1.0/CONTRIBUTING.md +53 -0
  15. unskein-0.1.0/LICENSE +21 -0
  16. unskein-0.1.0/PKG-INFO +222 -0
  17. unskein-0.1.0/README.md +192 -0
  18. unskein-0.1.0/SECURITY.md +21 -0
  19. unskein-0.1.0/docs/architecture.md +1057 -0
  20. unskein-0.1.0/docs/assets/banner.svg +31 -0
  21. unskein-0.1.0/pyproject.toml +113 -0
  22. unskein-0.1.0/scripts/gen_banner.py +123 -0
  23. unskein-0.1.0/src/unskein/__init__.py +10 -0
  24. unskein-0.1.0/src/unskein/__main__.py +5 -0
  25. unskein-0.1.0/src/unskein/ai/__init__.py +6 -0
  26. unskein-0.1.0/src/unskein/ai/client.py +269 -0
  27. unskein-0.1.0/src/unskein/ai/models.py +172 -0
  28. unskein-0.1.0/src/unskein/ai/prompts.py +271 -0
  29. unskein-0.1.0/src/unskein/cli.py +297 -0
  30. unskein-0.1.0/src/unskein/config.py +303 -0
  31. unskein-0.1.0/src/unskein/errors.py +59 -0
  32. unskein-0.1.0/src/unskein/graph/__init__.py +6 -0
  33. unskein-0.1.0/src/unskein/graph/builder.py +37 -0
  34. unskein-0.1.0/src/unskein/graph/metrics.py +186 -0
  35. unskein-0.1.0/src/unskein/i18n.py +304 -0
  36. unskein-0.1.0/src/unskein/logging_setup.py +65 -0
  37. unskein-0.1.0/src/unskein/parsers/__init__.py +6 -0
  38. unskein-0.1.0/src/unskein/parsers/base.py +109 -0
  39. unskein-0.1.0/src/unskein/parsers/discovery.py +134 -0
  40. unskein-0.1.0/src/unskein/parsers/indirection.py +97 -0
  41. unskein-0.1.0/src/unskein/parsers/models.py +176 -0
  42. unskein-0.1.0/src/unskein/parsers/python_parser.py +392 -0
  43. unskein-0.1.0/src/unskein/perf.py +59 -0
  44. unskein-0.1.0/src/unskein/pipeline.py +232 -0
  45. unskein-0.1.0/src/unskein/report/__init__.py +5 -0
  46. unskein-0.1.0/src/unskein/report/markdown.py +341 -0
  47. unskein-0.1.0/src/unskein/scan.py +225 -0
  48. unskein-0.1.0/tests/conftest.py +137 -0
  49. unskein-0.1.0/tests/fixtures/circular_imports/app/__init__.py +0 -0
  50. unskein-0.1.0/tests/fixtures/circular_imports/app/a.py +5 -0
  51. unskein-0.1.0/tests/fixtures/circular_imports/app/b.py +5 -0
  52. unskein-0.1.0/tests/fixtures/reexport_chain/app/__init__.py +3 -0
  53. unskein-0.1.0/tests/fixtures/reexport_chain/app/consumer.py +5 -0
  54. unskein-0.1.0/tests/fixtures/reexport_chain/app/core/__init__.py +3 -0
  55. unskein-0.1.0/tests/fixtures/reexport_chain/app/core/impl/__init__.py +0 -0
  56. unskein-0.1.0/tests/fixtures/reexport_chain/app/core/impl/engine.py +2 -0
  57. unskein-0.1.0/tests/fixtures/reexport_cycle/app/__init__.py +0 -0
  58. unskein-0.1.0/tests/fixtures/reexport_cycle/app/a/__init__.py +1 -0
  59. unskein-0.1.0/tests/fixtures/reexport_cycle/app/b/__init__.py +1 -0
  60. unskein-0.1.0/tests/fixtures/reexport_cycle/app/user.py +1 -0
  61. unskein-0.1.0/tests/fixtures/simple_project/app/__init__.py +0 -0
  62. unskein-0.1.0/tests/fixtures/simple_project/app/main.py +7 -0
  63. unskein-0.1.0/tests/fixtures/simple_project/app/models.py +2 -0
  64. unskein-0.1.0/tests/fixtures/simple_project/app/services/__init__.py +0 -0
  65. unskein-0.1.0/tests/fixtures/simple_project/app/services/user.py +6 -0
  66. unskein-0.1.0/tests/integration/test_ai_isolation.py +64 -0
  67. unskein-0.1.0/tests/integration/test_cli_ai.py +152 -0
  68. unskein-0.1.0/tests/integration/test_cli_scan.py +215 -0
  69. unskein-0.1.0/tests/integration/test_docstrings.py +45 -0
  70. unskein-0.1.0/tests/integration/test_self_analysis.py +20 -0
  71. unskein-0.1.0/tests/unit/test_ai_client.py +278 -0
  72. unskein-0.1.0/tests/unit/test_ai_models.py +39 -0
  73. unskein-0.1.0/tests/unit/test_ai_prompts.py +440 -0
  74. unskein-0.1.0/tests/unit/test_analyze.py +50 -0
  75. unskein-0.1.0/tests/unit/test_config.py +218 -0
  76. unskein-0.1.0/tests/unit/test_discovery.py +108 -0
  77. unskein-0.1.0/tests/unit/test_discovery_symlinks.py +63 -0
  78. unskein-0.1.0/tests/unit/test_graph_builder.py +66 -0
  79. unskein-0.1.0/tests/unit/test_graph_metrics.py +187 -0
  80. unskein-0.1.0/tests/unit/test_i18n.py +97 -0
  81. unskein-0.1.0/tests/unit/test_import_collection.py +90 -0
  82. unskein-0.1.0/tests/unit/test_logging_setup.py +40 -0
  83. unskein-0.1.0/tests/unit/test_parse_plan.py +48 -0
  84. unskein-0.1.0/tests/unit/test_perf.py +27 -0
  85. unskein-0.1.0/tests/unit/test_pipeline_parallel.py +264 -0
  86. unskein-0.1.0/tests/unit/test_python_parse.py +268 -0
  87. unskein-0.1.0/tests/unit/test_python_parser.py +124 -0
  88. unskein-0.1.0/tests/unit/test_reexport_resolution.py +156 -0
  89. unskein-0.1.0/tests/unit/test_report_markdown.py +296 -0
  90. unskein-0.1.0/tests/unit/test_scan.py +229 -0
  91. unskein-0.1.0/uv.lock +2166 -0
@@ -0,0 +1,10 @@
1
+ # Copy to .env and fill in. Never commit .env.
2
+ # Precedence: env var > .unskein.toml / ~/.config/unskein/config.toml > --api-key flag.
3
+
4
+ # LiteLLM model string, e.g. "ollama/qwen2.5-coder:7b" or "openai/gpt-4o-mini"
5
+ UNSKEIN_AI_MODEL=
6
+ UNSKEIN_API_KEY=
7
+ UNSKEIN_AI_API_BASE=
8
+
9
+ # "es" | "en" (default: system locale, falls back to English)
10
+ UNSKEIN_LANG=
@@ -0,0 +1,36 @@
1
+ name: Bug report
2
+ description: Something doesn't work as expected
3
+ labels: ["bug"]
4
+ body:
5
+ - type: textarea
6
+ id: what-happened
7
+ attributes:
8
+ label: What happened?
9
+ description: Include the command you ran and the output. Remove any API keys.
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: expected
14
+ attributes:
15
+ label: What did you expect?
16
+ validations:
17
+ required: true
18
+ - type: textarea
19
+ id: repro
20
+ attributes:
21
+ label: Minimal reproduction
22
+ description: A small project layout (files + imports) that triggers the problem.
23
+ - type: input
24
+ id: version
25
+ attributes:
26
+ label: unskein version
27
+ placeholder: unskein --version
28
+ validations:
29
+ required: true
30
+ - type: input
31
+ id: python
32
+ attributes:
33
+ label: Python version and OS
34
+ placeholder: 3.14 on Ubuntu 24.04
35
+ validations:
36
+ required: true
@@ -0,0 +1 @@
1
+ blank_issues_enabled: false
@@ -0,0 +1,19 @@
1
+ name: Feature request
2
+ description: Suggest an idea or improvement
3
+ labels: ["enhancement"]
4
+ body:
5
+ - type: textarea
6
+ id: problem
7
+ attributes:
8
+ label: Problem
9
+ description: What are you trying to do that unskein doesn't support?
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: proposal
14
+ attributes:
15
+ label: Proposed solution
16
+ - type: textarea
17
+ id: alternatives
18
+ attributes:
19
+ label: Alternatives considered
@@ -0,0 +1,12 @@
1
+ ## Summary
2
+
3
+ <!-- What does this change and why? Link the related issue. -->
4
+
5
+ ## Checklist
6
+
7
+ - [ ] Tests added or updated
8
+ - [ ] Clean Code rules followed; Google-style docstrings on every new or changed module, class and function (private ones included)
9
+ - [ ] `uv run pytest` and `uv run ruff check .` pass locally
10
+ - [ ] User-facing strings added to `unskein.i18n` in both `es` and `en`
11
+ - [ ] `CHANGELOG.md` updated under `[Unreleased]`
12
+ - [ ] No design decision from `docs/architecture.md` changed without prior discussion
@@ -0,0 +1,36 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, develop]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ lint:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v6
17
+ - run: uv sync --locked
18
+ - run: uv run ruff check .
19
+ - run: uv run ruff format --check .
20
+
21
+ test:
22
+ strategy:
23
+ fail-fast: false
24
+ matrix:
25
+ os: [ubuntu-latest, windows-latest]
26
+ python-version: ["3.12", "3.13", "3.14"]
27
+ runs-on: ${{ matrix.os }}
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - uses: astral-sh/setup-uv@v6
31
+ with:
32
+ python-version: ${{ matrix.python-version }}
33
+ - run: uv sync --locked
34
+ - run: uv run pytest --cov --cov-report=term-missing
35
+ - name: Smoke test - scan unskein itself end to end
36
+ run: uv run unskein scan src --no-ai --lang en
@@ -0,0 +1,33 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: astral-sh/setup-uv@v6
16
+ - run: uv build
17
+ - uses: actions/upload-artifact@v4
18
+ with:
19
+ name: dist
20
+ path: dist/
21
+
22
+ publish:
23
+ needs: build
24
+ runs-on: ubuntu-latest
25
+ environment: pypi
26
+ permissions:
27
+ id-token: write # PyPI Trusted Publishing (OIDC), no stored token
28
+ steps:
29
+ - uses: actions/download-artifact@v4
30
+ with:
31
+ name: dist
32
+ path: dist/
33
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,40 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Testing / coverage
13
+ .pytest_cache/
14
+ .coverage
15
+ .coverage.*
16
+ coverage.xml
17
+ htmlcov/
18
+ .ruff_cache/
19
+
20
+ # Secrets and local config (never commit API keys)
21
+ .env
22
+ .env.*
23
+ !.env.example
24
+ .unskein.toml
25
+
26
+ # Logs
27
+ *.log
28
+
29
+ # graft's local graph cache — regenerable, not committed (run `graft build`).
30
+ graft/
31
+
32
+ # Personal editor/agent tooling (graft hooks, MCP config) — per-developer, not shared
33
+ .claude/
34
+ .mcp.json
35
+ .ignore
36
+
37
+ docs/superpowers/
38
+ docs/litellm/
39
+ docs/code-reviews/
40
+ .superpowers/
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1,19 @@
1
+ # Copy to .unskein.toml (gitignored). Env vars take precedence over this file.
2
+
3
+ [general]
4
+ lang = "en" # "es" | "en"
5
+
6
+ [ai]
7
+ model = "ollama/qwen2.5-coder:7b"
8
+ api_base = "http://localhost:11434"
9
+ # api_key = "" # prefer UNSKEIN_API_KEY env var
10
+
11
+ [analysis]
12
+ parallel_threshold = 50
13
+ # max_workers = 8 # default: os.cpu_count()
14
+ queue_maxsize = 200
15
+ max_file_size_bytes = 5242880
16
+ per_file_timeout_seconds = 30
17
+ # default_encoding = "utf-8" # fallback only, never overrides a PEP 263 cookie
18
+ # source_roots = ["src", "lib"] # default: auto-detect src/ layout; project root is always a fallback
19
+ # include_tests = false # tests/, test_*.py, *_test.py, conftest.py are skipped by default
@@ -0,0 +1,111 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-09-30
11
+
12
+ ### Added
13
+
14
+ - AI interpretation: with a model configured, the report gains a summary, an
15
+ architecture health rating and problems with severity, produced through LiteLLM (any
16
+ provider, local Ollama or a LiteLLM Proxy). The AI only sees module names and
17
+ metrics; its answer is checked against the dependency graph, so it cannot name a
18
+ module that does not exist. If the call or the answer fails, the report is still
19
+ produced with a notice saying why. `--min-severity` and exit code 2 now apply to it.
20
+ Loosely written module names (quoted, with a trailing dot, or as file paths) are
21
+ matched to the graph; problems that still name no real module are discarded, and the
22
+ console and the report say how many.
23
+ - Initial project scaffold: `src/` layout, pipeline contracts, test fixtures, CI.
24
+ - Python import parser: absolute, relative and submodule imports, `is_external`
25
+ detection, re-export detection in `__init__.py`, per-file warnings instead of crashes
26
+ (syntax errors, undecodable files, oversized files, deeply nested code).
27
+ - Automatic `src/` layout detection and configurable `source_roots`.
28
+ - Test code excluded from analysis by default; `--include-tests` to opt back in.
29
+ - Re-export resolution: imports through `__init__.py` facades now point at the module
30
+ that defines the symbol, so cycles hidden behind a package facade become visible.
31
+ Re-export cycles produce a single warning per cycle.
32
+ - Dependency graph and coupling metrics: afferent/efferent coupling and instability per
33
+ module, dependency cycles (capped at 100, deterministic order) and the most coupled
34
+ modules by nearest-rank percentile of `Ca + Ce`.
35
+ - Configuration loading: `~/.config/unskein/config.toml` merged with the project's
36
+ `.unskein.toml` (project wins), validated so typos and wrong types are reported with
37
+ the file and field; env vars and CLI flags on top. API keys never appear in `repr`
38
+ or error messages.
39
+ - `--no-include-tests` and `--no-follow-symlinks`, so a flag can override
40
+ `.unskein.toml` in both directions.
41
+ - `unskein scan` works end to end: Markdown report in English or Spanish (terminal via
42
+ rich, raw file with `-o`), warnings grouped by kind with paths relative to the
43
+ project, `--verbose` stats and `--log-file`. The AI section explains why it is
44
+ empty (disabled, not configured, or the call failed).
45
+ - Exit codes are enforced: usage errors now exit with 1 (click's default 2 collided with
46
+ "high-severity problems found"), unexpected errors exit with 3 showing the traceback.
47
+ - Pylint configuration aligned with the Clean Code rules, for IDEs.
48
+ - Tangles: groups of mutually dependent modules (strongly connected components),
49
+ exact and never truncated, shown before the cycle list with their size, so a
50
+ "100+ cycles" report reveals it is really one knot of, say, 279 modules (#8).
51
+
52
+ - Parallel parsing: projects with 500 files or more are parsed in a process pool
53
+ (`parallel_threshold`, `max_workers`, `queue_maxsize` in `.unskein.toml`), up to
54
+ 2.9x faster on large projects (1.0-2.6x where processes are spawned, as on Windows), with the same result and order as the sequential parser. A
55
+ file that exceeds `per_file_timeout_seconds` is skipped with a warning; a worker that
56
+ dies makes the parser fall back to sequential. The timeout only stops waiting: a
57
+ worker stuck on a pathological file still delays closing the pool.
58
+
59
+ ### Fixed
60
+
61
+ - The list of dependency cycles (and which ones survived the 100-cycle limit) changed
62
+ between runs because networkx follows the string hash seed. Cycles are now enumerated
63
+ over integer labels, so the report is identical on every run.
64
+ - The README example `--exclude "migrations/**"` only skipped a `migrations/` folder at
65
+ the project root, not the ones inside each app. It now reads `--exclude "migrations/"`,
66
+ which matches at any depth (a pattern with a slash in the middle is anchored to the
67
+ root, as in `.gitignore`).
68
+ - `--verbose` now reports the real peak memory of the run. It used to read the resident
69
+ memory after the analysis, so memory freed before the end was invisible (a 200 MB
70
+ spike showed as 21 MB) (#9).
71
+ - Printing the report to a Windows pipe (cp1252) no longer crashes with an internal
72
+ error on symbols such as "→"; unencodable characters are replaced.
73
+
74
+ ### Changed
75
+
76
+ - The default `parallel_threshold` is now 500 files (was 50) and `max_workers` defaults to
77
+ the core count capped at 8. Measured, a pool at 50 files was 2-40x slower than
78
+ sequential parsing because starting it costs 130-300 ms.
79
+ - `LanguageAdapter.parse` is now concrete, built on the new `plan_parse` and `parse_task`
80
+ that language adapters implement, so the parallel and sequential paths share one code
81
+ path. `FileParseResult` moved to `unskein.parsers.models`.
82
+ - File discovery no longer enters excluded directories (`.venv/`, `build/`,
83
+ `node_modules/`, `tests/`…), so its cost no longer grows with ignored content. As in
84
+ git, a negated pattern (`!build/keep.py`) cannot re-include a file inside an excluded
85
+ directory: such a file is no longer analyzed.
86
+ - Import collection walks only statement lists instead of every AST node, making parsing
87
+ 1.3-1.5x faster on large projects (networkx, Django, sympy) with the same graph.
88
+ Imports are now visited depth-first in code order, so a file's warnings come out in
89
+ line order and the report may show different examples per warning kind.
90
+ - When a package `__init__.py` re-exports the same symbol more than once (for example a
91
+ `try/except ImportError` fallback), the first one in the code now wins; before, the
92
+ last one did.
93
+ - The placeholder notice about the AI arriving later is gone: the report now says
94
+ either that the AI is disabled or not configured, or why the call failed.
95
+ - Precedence for non-secret options is now flag > env var > `.unskein.toml` (so
96
+ `--lang` beats `UNSKEIN_LANG`); secrets keep env var > `.unskein.toml` > `--api-key`.
97
+ - Analysis warnings are now structured (`ParseWarning` with a `WarningCode`, file, line
98
+ and language-neutral detail) instead of English strings, so reports can translate
99
+ them and group them by kind.
100
+ - Clean Code is now a mandatory project convention: Google-style docstrings on every
101
+ module, class and function (private ones included), enforced in CI with ruff's `D`
102
+ rules plus an AST-based test that also covers private names.
103
+ - Parser data classes moved from `unskein.parsers.base` to `unskein.parsers.models`
104
+ (still importable from `unskein.parsers`), removing an import cycle.
105
+ - LiteLLM is now required at `>=1.95.0,<2`. The AI client resolves how to call the
106
+ model once (reasoning models get `temperature=1.0`, JSON-schema output when the model
107
+ supports it, a 60 s limit for direct calls) and never lets LiteLLM download its price
108
+ map or send telemetry.
109
+
110
+ [Unreleased]: https://github.com/mapenzo/unskein/compare/v0.1.0...HEAD
111
+ [0.1.0]: https://github.com/mapenzo/unskein/releases/tag/v0.1.0