modwire-cli 1.0.0__tar.gz → 1.0.1__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 (57) hide show
  1. {modwire_cli-1.0.0 → modwire_cli-1.0.1}/.gitignore +2 -7
  2. modwire_cli-1.0.1/PKG-INFO +72 -0
  3. modwire_cli-1.0.1/README.md +37 -0
  4. modwire_cli-1.0.1/pyproject.toml +84 -0
  5. modwire_cli-1.0.1/src/modwire_cli/__init__.py +3 -0
  6. modwire_cli-1.0.1/src/modwire_cli/__main__.py +38 -0
  7. {modwire_cli-1.0.0 → modwire_cli-1.0.1}/src/modwire_cli/_version.py +3 -3
  8. modwire_cli-1.0.1/src/modwire_cli/config.py +15 -0
  9. modwire_cli-1.0.1/src/modwire_cli/extraction.py +14 -0
  10. modwire_cli-1.0.1/src/modwire_cli/facade.py +30 -0
  11. modwire_cli-1.0.1/src/modwire_cli/pipeline/__init__.py +0 -0
  12. modwire_cli-1.0.1/src/modwire_cli/pipeline/context.py +26 -0
  13. modwire_cli-1.0.1/src/modwire_cli/pipeline/runner.py +32 -0
  14. modwire_cli-1.0.1/src/modwire_cli/pipeline/step.py +13 -0
  15. modwire_cli-1.0.1/src/modwire_cli/pipeline/steps/__init__.py +0 -0
  16. modwire_cli-1.0.1/src/modwire_cli/pipeline/steps/callables_insight.py +24 -0
  17. modwire_cli-1.0.1/src/modwire_cli/pipeline/steps/clusters_insight.py +28 -0
  18. modwire_cli-1.0.1/src/modwire_cli/pipeline/steps/coherence_insight.py +37 -0
  19. modwire_cli-1.0.1/src/modwire_cli/pipeline/steps/flow_violations.py +24 -0
  20. modwire_cli-1.0.1/src/modwire_cli/pipeline/steps/hotspots_insight.py +26 -0
  21. modwire_cli-1.0.1/src/modwire_cli/pipeline/steps/map_tree.py +34 -0
  22. modwire_cli-1.0.1/src/modwire_cli/pipeline/steps/shape_violations.py +27 -0
  23. modwire_cli-1.0.1/src/modwire_cli/pipeline/steps/success.py +16 -0
  24. modwire_cli-1.0.1/src/modwire_cli/pipeline/steps/unknown_files_warning.py +20 -0
  25. modwire_cli-1.0.1/src/modwire_cli/pipeline/steps/unused_exports_insight.py +26 -0
  26. modwire_cli-1.0.1/tests/test_cli.py +60 -0
  27. modwire_cli-1.0.0/.github/ISSUE_TEMPLATE/bug_report.yml +0 -46
  28. modwire_cli-1.0.0/.github/ISSUE_TEMPLATE/config.yml +0 -1
  29. modwire_cli-1.0.0/.github/ISSUE_TEMPLATE/feature_request.yml +0 -47
  30. modwire_cli-1.0.0/.github/ISSUE_TEMPLATE/task.yml +0 -48
  31. modwire_cli-1.0.0/.github/workflows/ci.yml +0 -28
  32. modwire_cli-1.0.0/.github/workflows/human-approval.yml +0 -133
  33. modwire_cli-1.0.0/.github/workflows/issue-metadata.yml +0 -21
  34. modwire_cli-1.0.0/.github/workflows/release.yml +0 -68
  35. modwire_cli-1.0.0/CHANGELOG.md +0 -11
  36. modwire_cli-1.0.0/MANIFEST.in +0 -5
  37. modwire_cli-1.0.0/Makefile +0 -21
  38. modwire_cli-1.0.0/PKG-INFO +0 -78
  39. modwire_cli-1.0.0/README.md +0 -42
  40. modwire_cli-1.0.0/pyproject.toml +0 -75
  41. modwire_cli-1.0.0/scripts/verify_release.py +0 -57
  42. modwire_cli-1.0.0/setup.cfg +0 -4
  43. modwire_cli-1.0.0/src/modwire_cli/__init__.py +0 -6
  44. modwire_cli-1.0.0/src/modwire_cli/main.py +0 -65
  45. modwire_cli-1.0.0/src/modwire_cli.egg-info/PKG-INFO +0 -78
  46. modwire_cli-1.0.0/src/modwire_cli.egg-info/SOURCES.txt +0 -31
  47. modwire_cli-1.0.0/src/modwire_cli.egg-info/dependency_links.txt +0 -1
  48. modwire_cli-1.0.0/src/modwire_cli.egg-info/entry_points.txt +0 -2
  49. modwire_cli-1.0.0/src/modwire_cli.egg-info/requires.txt +0 -9
  50. modwire_cli-1.0.0/src/modwire_cli.egg-info/scm_file_list.json +0 -26
  51. modwire_cli-1.0.0/src/modwire_cli.egg-info/scm_version.json +0 -8
  52. modwire_cli-1.0.0/src/modwire_cli.egg-info/top_level.txt +0 -1
  53. modwire_cli-1.0.0/tests/test_cli.py +0 -65
  54. modwire_cli-1.0.0/tests/test_release.py +0 -9
  55. modwire_cli-1.0.0/uv.lock +0 -867
  56. {modwire_cli-1.0.0 → modwire_cli-1.0.1}/LICENSE +0 -0
  57. {modwire_cli-1.0.0 → modwire_cli-1.0.1}/src/modwire_cli/py.typed +0 -0
@@ -1,12 +1,7 @@
1
1
  .venv/
2
- .dev/
3
2
  .pytest_cache/
4
3
  .ruff_cache/
5
- .coverage
6
- htmlcov/
7
- build/
8
- dist/
9
- *.egg-info/
10
- src/modwire_cli/_version.py
11
4
  __pycache__/
12
5
  *.py[cod]
6
+ dist/
7
+ src/modwire_cli/_version.py
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: modwire-cli
3
+ Version: 1.0.1
4
+ Summary: Command-line interface for the Modwire ecosystem.
5
+ Project-URL: Homepage, https://github.com/modwire/modwire-cli
6
+ Project-URL: Repository, https://github.com/modwire/modwire-cli
7
+ Project-URL: Issues, https://github.com/modwire/modwire-cli/issues
8
+ Author: Tomasz Szpak
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: cli,modwire
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Software Development
23
+ Requires-Python: >=3.12
24
+ Requires-Dist: modwire-architecture<7,>=6
25
+ Requires-Dist: modwire-extraction<3,>=2
26
+ Requires-Dist: pydantic-yaml>=1.7
27
+ Requires-Dist: rich>=13
28
+ Requires-Dist: wireup>=2.12.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: build>=1.3; extra == 'dev'
31
+ Requires-Dist: pytest>=9; extra == 'dev'
32
+ Requires-Dist: ruff>=0.14; extra == 'dev'
33
+ Requires-Dist: twine>=6; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # modwire-cli
37
+
38
+ The command-line interface for the Modwire ecosystem.
39
+
40
+ ## Install
41
+
42
+ ```sh
43
+ pip install modwire-cli
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ ```sh
49
+ modwire --language python
50
+ ```
51
+
52
+ By default, Modwire reads its configuration from `.modwire/architecture.yaml`
53
+ and analyses the current directory. Override either path when needed:
54
+
55
+ ```sh
56
+ modwire --language typescript --dot-dir path/to/.modwire --architecture-root path/to/source
57
+ ```
58
+
59
+ To check this repository's architecture:
60
+
61
+ ```sh
62
+ uv run modwire --language python
63
+ ```
64
+
65
+ ## Development
66
+
67
+ ```sh
68
+ uv sync --all-groups
69
+ uv run pytest
70
+ uv run ruff check .
71
+ uv build
72
+ ```
@@ -0,0 +1,37 @@
1
+ # modwire-cli
2
+
3
+ The command-line interface for the Modwire ecosystem.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pip install modwire-cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```sh
14
+ modwire --language python
15
+ ```
16
+
17
+ By default, Modwire reads its configuration from `.modwire/architecture.yaml`
18
+ and analyses the current directory. Override either path when needed:
19
+
20
+ ```sh
21
+ modwire --language typescript --dot-dir path/to/.modwire --architecture-root path/to/source
22
+ ```
23
+
24
+ To check this repository's architecture:
25
+
26
+ ```sh
27
+ uv run modwire --language python
28
+ ```
29
+
30
+ ## Development
31
+
32
+ ```sh
33
+ uv sync --all-groups
34
+ uv run pytest
35
+ uv run ruff check .
36
+ uv build
37
+ ```
@@ -0,0 +1,84 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.28", "hatch-vcs>=0.5"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "modwire-cli"
7
+ dynamic = ["version"]
8
+ description = "Command-line interface for the Modwire ecosystem."
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = "MIT"
12
+ authors = [{ name = "Tomasz Szpak" }]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Topic :: Software Development",
25
+ ]
26
+ keywords = ["cli", "modwire"]
27
+ dependencies = [
28
+ "modwire-architecture>=6,<7",
29
+ "modwire-extraction>=2,<3",
30
+ "pydantic-yaml>=1.7",
31
+ "rich>=13",
32
+ "wireup>=2.12.0",
33
+ ]
34
+
35
+ [project.scripts]
36
+ modwire = "modwire_cli.__main__:main"
37
+
38
+ [project.optional-dependencies]
39
+ dev = [
40
+ "build>=1.3",
41
+ "pytest>=9",
42
+ "ruff>=0.14",
43
+ "twine>=6",
44
+ ]
45
+
46
+ [project.urls]
47
+ Homepage = "https://github.com/modwire/modwire-cli"
48
+ Repository = "https://github.com/modwire/modwire-cli"
49
+ Issues = "https://github.com/modwire/modwire-cli/issues"
50
+
51
+ [dependency-groups]
52
+ dev = [
53
+ "build>=1.3",
54
+ "pytest>=9",
55
+ "ruff>=0.14",
56
+ "twine>=6",
57
+ ]
58
+
59
+ [tool.hatch.version]
60
+ source = "vcs"
61
+ tag-pattern = '^v(?P<version>\d+\.\d+\.\d+)$'
62
+ fallback-version = "0.0.0"
63
+
64
+ [tool.hatch.build.hooks.vcs]
65
+ version-file = "src/modwire_cli/_version.py"
66
+
67
+ [tool.hatch.build.targets.wheel]
68
+ packages = ["src/modwire_cli"]
69
+
70
+ [tool.hatch.build.targets.sdist]
71
+ include = ["src", "tests", "README.md", "LICENSE", "pyproject.toml"]
72
+
73
+ [tool.pytest.ini_options]
74
+ testpaths = ["tests"]
75
+ addopts = ["-ra", "--strict-config", "--strict-markers"]
76
+ filterwarnings = ["error"]
77
+
78
+ [tool.ruff]
79
+ line-length = 120
80
+ extend-exclude = ["**/_version.py"]
81
+ target-version = "py312"
82
+
83
+ [tool.ruff.lint]
84
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
@@ -0,0 +1,3 @@
1
+ from ._version import __version__
2
+
3
+ __all__ = ["__version__"]
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ from collections.abc import Sequence
6
+ from pathlib import Path
7
+
8
+ from rich.console import Console
9
+ from wireup import create_sync_container, instance
10
+
11
+ import modwire_cli
12
+
13
+ from .facade import FitnessFacade
14
+
15
+
16
+ def main() -> int:
17
+ return run(sys.argv[1:])
18
+
19
+
20
+ def run(argv: Sequence[str]) -> int:
21
+ parser = argparse.ArgumentParser()
22
+ parser.add_argument("--dot-dir", type=Path, default=Path(".modwire"))
23
+ parser.add_argument("--architecture-root", type=Path, default=Path("."))
24
+ parser.add_argument("--language", required=True)
25
+ arguments = parser.parse_args(argv)
26
+
27
+ container = create_sync_container(
28
+ injectables=[modwire_cli, instance(Console(), as_type=Console)]
29
+ )
30
+ try:
31
+ facade = container.get(FitnessFacade)
32
+ return facade.reports(
33
+ dot_dir=arguments.dot_dir,
34
+ architecture_root=arguments.architecture_root,
35
+ language=arguments.language,
36
+ )
37
+ finally:
38
+ container.close()
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.0.0'
22
- __version_tuple__ = version_tuple = (1, 0, 0)
21
+ __version__ = version = '1.0.1'
22
+ __version_tuple__ = version_tuple = (1, 0, 1)
23
23
 
24
- __commit_id__ = commit_id = 'ge8788a19e'
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from modwire_architecture import ArchitectureConfig
7
+ from pydantic_yaml import parse_yaml_file_as
8
+ from wireup import injectable
9
+
10
+
11
+ @injectable
12
+ @dataclass(frozen=True)
13
+ class ConfigService:
14
+ def load(self, dot_dir: Path) -> ArchitectureConfig:
15
+ return parse_yaml_file_as(ArchitectureConfig, dot_dir / "architecture.yaml")
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from modwire_extraction import ModwireExtraction, QueryableCodeMap
7
+ from wireup import injectable
8
+
9
+
10
+ @injectable
11
+ @dataclass(frozen=True)
12
+ class ExtractionService:
13
+ def load(self, architecture_root: Path, language: str) -> QueryableCodeMap:
14
+ return ModwireExtraction(architecture_root).generate_queryable_map(language)
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from modwire_architecture import Modwire
7
+ from wireup import injectable
8
+
9
+ from .config import ConfigService
10
+ from .extraction import ExtractionService
11
+ from .pipeline.runner import ReportPipelineRunner
12
+
13
+
14
+ @injectable
15
+ @dataclass(frozen=True)
16
+ class FitnessFacade:
17
+ config_service: ConfigService
18
+ extraction_service: ExtractionService
19
+ pipeline_runner: ReportPipelineRunner
20
+
21
+ def reports(
22
+ self,
23
+ dot_dir: Path,
24
+ architecture_root: Path,
25
+ language: str,
26
+ ) -> int:
27
+ config = self.config_service.load(dot_dir)
28
+ code_map = self.extraction_service.load(architecture_root, language)
29
+ reports = Modwire().architecture(config).report(code_map)
30
+ return self.pipeline_runner.run(reports)
File without changes
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, replace
4
+ from typing import cast
5
+
6
+ from modwire_architecture.shared.report.base import ReportNode
7
+ from rich.console import Console
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class ReportPipelineContext:
12
+ reports: tuple[ReportNode, ...]
13
+ console: Console
14
+ failed: bool = False
15
+
16
+ def has_report(self, report_type: type[ReportNode]) -> bool:
17
+ return any(type(report) is report_type for report in self.reports)
18
+
19
+ def report[ReportType: ReportNode](self, report_type: type[ReportType]) -> ReportType:
20
+ for report in self.reports:
21
+ if type(report) is report_type:
22
+ return cast(ReportType, report)
23
+ raise LookupError(f"Missing report: {report_type.__name__}")
24
+
25
+ def fail(self) -> ReportPipelineContext:
26
+ return replace(self, failed=True)
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Hashable, Mapping
4
+ from dataclasses import dataclass
5
+
6
+ from modwire_architecture.shared.report.base import ReportNode
7
+ from rich.console import Console
8
+ from wireup import injectable
9
+
10
+ from .context import ReportPipelineContext
11
+ from .step import ReportPipelineStep
12
+
13
+
14
+ @injectable
15
+ @dataclass(frozen=True)
16
+ class ReportPipelineRunner:
17
+ steps: Mapping[Hashable, ReportPipelineStep]
18
+ console: Console
19
+
20
+ def run(self, reports: tuple[ReportNode, ...]) -> int:
21
+ context = ReportPipelineContext(reports=reports, console=self.console)
22
+ result = self.process(context)
23
+ return int(result.failed)
24
+
25
+ def process(self, context: ReportPipelineContext) -> ReportPipelineContext:
26
+ for _, step in sorted(
27
+ self.steps.items(),
28
+ key=lambda item: int(str(item[0]).partition("-")[0]),
29
+ ):
30
+ if step.should_process(context):
31
+ context = step.process(context)
32
+ return context
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ from .context import ReportPipelineContext
6
+
7
+
8
+ class ReportPipelineStep(ABC):
9
+ @abstractmethod
10
+ def should_process(self, context: ReportPipelineContext) -> bool: ...
11
+
12
+ @abstractmethod
13
+ def process(self, context: ReportPipelineContext) -> ReportPipelineContext: ...
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from modwire_architecture.architecture.insights.reporters.callables import CallablesReport
4
+ from wireup import injectable
5
+
6
+ from ..context import ReportPipelineContext
7
+ from ..step import ReportPipelineStep
8
+
9
+
10
+ @injectable(as_type=ReportPipelineStep, qualifier="80-callables")
11
+ class CallablesInsight(ReportPipelineStep):
12
+ def should_process(self, context: ReportPipelineContext) -> bool:
13
+ return context.has_report(CallablesReport) and bool(context.report(CallablesReport).entries)
14
+
15
+ def process(self, context: ReportPipelineContext) -> ReportPipelineContext:
16
+ report = context.report(CallablesReport)
17
+ context.console.print("[blue]Callable graph:[/blue]")
18
+ for entry in report.entries:
19
+ context.console.print(entry.source_callable, markup=False)
20
+ for call in entry.calls:
21
+ context.console.print(f" calls: {call}", markup=False)
22
+ for caller in entry.callers:
23
+ context.console.print(f" called by: {caller}", markup=False)
24
+ return context
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from modwire_architecture.architecture.insights.reporters.clusters import ClustersReport
4
+ from wireup import injectable
5
+
6
+ from ..context import ReportPipelineContext
7
+ from ..step import ReportPipelineStep
8
+
9
+
10
+ @injectable(as_type=ReportPipelineStep, qualifier="50-clusters")
11
+ class ClustersInsight(ReportPipelineStep):
12
+ def should_process(self, context: ReportPipelineContext) -> bool:
13
+ return context.has_report(ClustersReport) and bool(context.report(ClustersReport).clusters)
14
+
15
+ def process(self, context: ReportPipelineContext) -> ReportPipelineContext:
16
+ report = context.report(ClustersReport)
17
+ context.console.print("[blue]Dependency clusters:[/blue]")
18
+ for cluster in report.clusters:
19
+ context.console.print(
20
+ (
21
+ f"{cluster.name} (pressure: {cluster.pressure_score}, "
22
+ f"incoming: {cluster.incoming_count}, outgoing: {cluster.outgoing_count})"
23
+ ),
24
+ markup=False,
25
+ )
26
+ for source_id in cluster.top_files:
27
+ context.console.print(f" - {source_id}", markup=False)
28
+ return context
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from modwire_architecture.architecture.insights.reporters.coherence import CoherenceReport
4
+ from wireup import injectable
5
+
6
+ from ..context import ReportPipelineContext
7
+ from ..step import ReportPipelineStep
8
+
9
+
10
+ @injectable(as_type=ReportPipelineStep, qualifier="70-coherence")
11
+ class CoherenceInsight(ReportPipelineStep):
12
+ def should_process(self, context: ReportPipelineContext) -> bool:
13
+ if not context.has_report(CoherenceReport):
14
+ return False
15
+ report = context.report(CoherenceReport)
16
+ return bool(report.roots or report.leaves or report.isolated or report.external_dependencies)
17
+
18
+ def process(self, context: ReportPipelineContext) -> ReportPipelineContext:
19
+ report = context.report(CoherenceReport)
20
+ context.console.print("[blue]Dependency coherence:[/blue]")
21
+ self._print_section(context, "Roots", report.roots)
22
+ self._print_section(context, "Leaves", report.leaves)
23
+ self._print_section(context, "Isolated", report.isolated)
24
+ self._print_section(context, "External dependencies", report.external_dependencies)
25
+ return context
26
+
27
+ def _print_section(
28
+ self,
29
+ context: ReportPipelineContext,
30
+ title: str,
31
+ entries: tuple[str, ...],
32
+ ) -> None:
33
+ if not entries:
34
+ return
35
+ context.console.print(title)
36
+ for entry in entries:
37
+ context.console.print(f" - {entry}", markup=False)
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from modwire_architecture.architecture.boundaries.collector import FlowReport
4
+ from wireup import injectable
5
+
6
+ from ..context import ReportPipelineContext
7
+ from ..step import ReportPipelineStep
8
+
9
+
10
+ @injectable(as_type=ReportPipelineStep, qualifier="30-flow-violations")
11
+ class FlowViolations(ReportPipelineStep):
12
+ def should_process(self, context: ReportPipelineContext) -> bool:
13
+ return bool(context.report(FlowReport).violations)
14
+
15
+ def process(self, context: ReportPipelineContext) -> ReportPipelineContext:
16
+ report = context.report(FlowReport)
17
+ context.console.print("[red]Flow violations:[/red]")
18
+ for violation in report.violations:
19
+ context.console.print(
20
+ f" - [{violation.rule_name}] {violation.message}",
21
+ markup=False,
22
+ )
23
+ context.console.print(f" {' -> '.join(violation.path)}", markup=False)
24
+ return context.fail()
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from modwire_architecture.architecture.insights.reporters.hotspots import HotspotsReport
4
+ from wireup import injectable
5
+
6
+ from ..context import ReportPipelineContext
7
+ from ..step import ReportPipelineStep
8
+
9
+
10
+ @injectable(as_type=ReportPipelineStep, qualifier="60-hotspots")
11
+ class HotspotsInsight(ReportPipelineStep):
12
+ def should_process(self, context: ReportPipelineContext) -> bool:
13
+ return context.has_report(HotspotsReport) and bool(context.report(HotspotsReport).hotspots)
14
+
15
+ def process(self, context: ReportPipelineContext) -> ReportPipelineContext:
16
+ report = context.report(HotspotsReport)
17
+ context.console.print("[blue]Dependency hotspots:[/blue]")
18
+ for hotspot in report.hotspots:
19
+ context.console.print(
20
+ (
21
+ f" - {hotspot.source_id} (pressure: {hotspot.pressure_score}, "
22
+ f"incoming: {hotspot.incoming_count}, outgoing: {hotspot.outgoing_count})"
23
+ ),
24
+ markup=False,
25
+ )
26
+ return context
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from modwire_architecture.architecture.map.collector import ArchitectureGroup, MapReport
4
+ from rich.tree import Tree
5
+ from wireup import injectable
6
+
7
+ from ..context import ReportPipelineContext
8
+ from ..step import ReportPipelineStep
9
+
10
+
11
+ @injectable(as_type=ReportPipelineStep, qualifier="10-map")
12
+ class MapTree(ReportPipelineStep):
13
+ def should_process(self, context: ReportPipelineContext) -> bool:
14
+ return True
15
+
16
+ def process(self, context: ReportPipelineContext) -> ReportPipelineContext:
17
+ report = context.report(MapReport)
18
+ tree = Tree("Architecture Map")
19
+ self._add_groups(tree, "Modules", report.modules)
20
+ self._add_groups(tree, "Layers", report.layers)
21
+ context.console.print(tree)
22
+ return context
23
+
24
+ def _add_groups(
25
+ self,
26
+ tree: Tree,
27
+ title: str,
28
+ groups: tuple[ArchitectureGroup, ...],
29
+ ) -> None:
30
+ branch = tree.add(title)
31
+ for group in groups:
32
+ group_branch = branch.add(group.name)
33
+ for source_id in group.source_ids:
34
+ group_branch.add(source_id)
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from modwire_architecture.architecture.shape.collector import ShapeReport
4
+ from wireup import injectable
5
+
6
+ from ..context import ReportPipelineContext
7
+ from ..step import ReportPipelineStep
8
+
9
+
10
+ @injectable(as_type=ReportPipelineStep, qualifier="40-shape-violations")
11
+ class ShapeViolations(ReportPipelineStep):
12
+ def should_process(self, context: ReportPipelineContext) -> bool:
13
+ return bool(context.report(ShapeReport).violations)
14
+
15
+ def process(self, context: ReportPipelineContext) -> ReportPipelineContext:
16
+ report = context.report(ShapeReport)
17
+ context.console.print("[red]Shape violations:[/red]")
18
+ for violation in report.violations:
19
+ symbol = f" {violation.symbol_kind} {violation.symbol_name}" if violation.symbol_kind else ""
20
+ context.console.print(
21
+ (
22
+ f" - [{violation.rule_name}] {violation.source_id}{symbol}: "
23
+ f"{violation.actual} exceeds {violation.limit}"
24
+ ),
25
+ markup=False,
26
+ )
27
+ return context.fail()
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from wireup import injectable
4
+
5
+ from ..context import ReportPipelineContext
6
+ from ..step import ReportPipelineStep
7
+
8
+
9
+ @injectable(as_type=ReportPipelineStep, qualifier="100-success")
10
+ class Success(ReportPipelineStep):
11
+ def should_process(self, context: ReportPipelineContext) -> bool:
12
+ return not context.failed
13
+
14
+ def process(self, context: ReportPipelineContext) -> ReportPipelineContext:
15
+ context.console.print("[green]Architecture checks passed.[/green]")
16
+ return context
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from modwire_architecture.architecture.map.collector import MapReport
4
+ from wireup import injectable
5
+
6
+ from ..context import ReportPipelineContext
7
+ from ..step import ReportPipelineStep
8
+
9
+
10
+ @injectable(as_type=ReportPipelineStep, qualifier="20-unknown-files")
11
+ class UnknownFilesWarning(ReportPipelineStep):
12
+ def should_process(self, context: ReportPipelineContext) -> bool:
13
+ return bool(context.report(MapReport).unknown_files)
14
+
15
+ def process(self, context: ReportPipelineContext) -> ReportPipelineContext:
16
+ report = context.report(MapReport)
17
+ context.console.print("[yellow]Warning:[/yellow] Files not included in the architecture map:")
18
+ for source_id in report.unknown_files:
19
+ context.console.print(f" - {source_id}", markup=False)
20
+ return context