roadmap-cli 0.2.0__tar.gz → 0.3.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.
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/.gitignore +1 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/CHANGELOG.md +32 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/PKG-INFO +6 -3
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/README.md +3 -1
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/pyproject.toml +15 -5
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/__init__.py +2 -2
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/__init__.py +7 -7
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/cleanup.py +62 -32
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/cli_command_helpers.py +51 -1
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/data/commands.py +88 -57
- roadmap_cli-0.3.0/roadmap/adapters/inbound/cli/exception_handler.py +37 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/health/commands.py +28 -15
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/init/commands.py +21 -11
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/issues/archive.py +27 -14
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/issues/list.py +55 -14
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/issues/query_presenter.py +44 -29
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/issues/resolution.py +10 -15
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/issues/restore.py +11 -6
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/layout.py +6 -3
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/migrate.py +32 -24
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/milestones/archive.py +21 -16
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/milestones/restore.py +5 -6
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/milestones/view.py +39 -22
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/models/output_models.py +42 -39
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/output_formatter.py +27 -33
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/planning_resolution.py +1 -15
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/projects/archive.py +21 -16
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/projects/list.py +28 -14
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/projects/restore.py +5 -6
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/status.py +60 -55
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/outbound/persistence/canonical.py +43 -78
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/outbound/persistence/configuration.py +47 -19
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/outbound/persistence/diagnostics.py +157 -118
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/outbound/persistence/documents.py +121 -89
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/outbound/persistence/migration.py +167 -127
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/application/ports.py +1 -1
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/application/use_cases/issue_mutations.py +83 -37
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/application/use_cases/issues.py +7 -6
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/application/use_cases/planning.py +116 -40
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/domain/aggregates.py +30 -16
- roadmap_cli-0.2.0/roadmap/adapters/inbound/cli/exception_handler.py +0 -94
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/LICENSE.md +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/__init__.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/__init__.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/analysis/__init__.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/analysis/commands.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/analysis/presenter.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/column_factory.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/config/__init__.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/config/commands.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/console.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/daily_summary.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/data/__init__.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/datetime_parser.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/decorators.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/git/__init__.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/git/commands.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/health/__init__.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/init/__init__.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/issues/__init__.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/issues/block.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/issues/close.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/issues/comment.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/issues/create.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/issues/delete.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/issues/deps.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/issues/progress.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/issues/start.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/issues/unblock.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/issues/update.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/issues/view.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/milestones/__init__.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/milestones/assign.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/milestones/close.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/milestones/create.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/milestones/delete.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/milestones/kanban.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/milestones/list.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/milestones/recalculate.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/milestones/update.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/models/__init__.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/output_options.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/projects/__init__.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/projects/close.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/projects/create.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/projects/delete.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/projects/update.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/projects/view.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/styling.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/inbound/cli/today.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/outbound/__init__.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/outbound/git/__init__.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/outbound/git/local.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/outbound/persistence/__init__.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/outbound/persistence/initialization.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/outbound/persistence/issue_queries.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/outbound/persistence/projection.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/adapters/outbound/system.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/application/__init__.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/application/configuration.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/application/contracts.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/application/failures.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/application/use_cases/__init__.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/application/use_cases/health.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/application/use_cases/local_git.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/application/use_cases/workspace_initialization.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/application/use_cases/workspace_migration.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/bootstrap/__init__.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/bootstrap/core.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/domain/__init__.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/domain/failures.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/domain/transitions.py +0 -0
- {roadmap_cli-0.2.0 → roadmap_cli-0.3.0}/roadmap/domain/types.py +0 -0
|
@@ -6,8 +6,40 @@ All notable changes to Roadmap CLI are recorded here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.0] - 2026-09-02
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Migrated static type checking from Pyright-only to `ty` running alongside
|
|
14
|
+
Pyright, both enforced in CI and pre-commit; aligned their configured
|
|
15
|
+
Python-version floor with the declared `>=3.12` support range.
|
|
16
|
+
- Reduced cyclomatic complexity across the codebase and enforced a
|
|
17
|
+
xenon B-or-better gate (absolute, per-module, and average) in CI and
|
|
18
|
+
pre-commit.
|
|
19
|
+
- Raised the enforced test coverage floor from 78% to 85%, backed by a
|
|
20
|
+
targeted test-coverage sweep (CLI output formatting and issue query
|
|
21
|
+
presentation) that reached 85.70%.
|
|
22
|
+
- Reduced DRY violations identified during the complexity-reduction pass.
|
|
23
|
+
- Enabled GitHub's native Dependabot vulnerability alerts for the repository.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Removed the stale, inaccurate `.env.production` file (unverifiable "0 known
|
|
28
|
+
CVEs" claim, an obsolete `poetry install` reference, and unused
|
|
29
|
+
environment variables that nothing in the codebase reads).
|
|
30
|
+
- Removed the stale, tracked `output.txt` pytest-output artifact from version
|
|
31
|
+
control.
|
|
32
|
+
- Fixed a broken `SECURITY.md` reference to a nonexistent developer-notes
|
|
33
|
+
file; it now points to the ADR that documents credential handling.
|
|
34
|
+
|
|
35
|
+
### Removed
|
|
36
|
+
|
|
37
|
+
- Removed `.env.production` and `output.txt` from version control (see
|
|
38
|
+
Fixed, above).
|
|
39
|
+
|
|
9
40
|
## [0.2.0] - 2026-08-27
|
|
10
41
|
|
|
42
|
+
|
|
11
43
|
### Added
|
|
12
44
|
|
|
13
45
|
- Added explicit `roadmap migrate` preflight, dry-run, confirmed execution,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: roadmap-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Git-native command line tool for local project roadmap management
|
|
5
5
|
Project-URL: Homepage, https://github.com/shanewilkins/roadmap
|
|
6
6
|
Project-URL: Repository, https://github.com/shanewilkins/roadmap
|
|
@@ -43,7 +43,8 @@ Requires-Dist: pytest-mock>=3.15.1; extra == 'dev'
|
|
|
43
43
|
Requires-Dist: pytest-xdist>=3.8.0; extra == 'dev'
|
|
44
44
|
Requires-Dist: pytest>=9.0.3; extra == 'dev'
|
|
45
45
|
Requires-Dist: radon<7.0.0,>=6.0.1; extra == 'dev'
|
|
46
|
-
Requires-Dist: ruff==0.16.
|
|
46
|
+
Requires-Dist: ruff==0.16.5; extra == 'dev'
|
|
47
|
+
Requires-Dist: ty>=0.0.77; extra == 'dev'
|
|
47
48
|
Requires-Dist: xenon<1.0.0,>=0.9.3; extra == 'dev'
|
|
48
49
|
Description-Content-Type: text/markdown
|
|
49
50
|
|
|
@@ -54,9 +55,11 @@ their planning data in reviewable Markdown and YAML files rather than a hosted
|
|
|
54
55
|
service. It supports issues, dependencies, comments, projects, milestones,
|
|
55
56
|
daily views, health checks, and machine-readable exports.
|
|
56
57
|
|
|
57
|
-
The current release is 0.
|
|
58
|
+
The current release is 0.3.0. Its deliberately contracted behavior is defined
|
|
58
59
|
by an explicit
|
|
59
60
|
[public compatibility contract](docs/architecture/public-contract-0.2.md).
|
|
61
|
+
0.3 is an internal quality-hardening release; the 0.2 CLI and data contract is
|
|
62
|
+
unchanged.
|
|
60
63
|
|
|
61
64
|
## What it does
|
|
62
65
|
|
|
@@ -5,9 +5,11 @@ their planning data in reviewable Markdown and YAML files rather than a hosted
|
|
|
5
5
|
service. It supports issues, dependencies, comments, projects, milestones,
|
|
6
6
|
daily views, health checks, and machine-readable exports.
|
|
7
7
|
|
|
8
|
-
The current release is 0.
|
|
8
|
+
The current release is 0.3.0. Its deliberately contracted behavior is defined
|
|
9
9
|
by an explicit
|
|
10
10
|
[public compatibility contract](docs/architecture/public-contract-0.2.md).
|
|
11
|
+
0.3 is an internal quality-hardening release; the 0.2 CLI and data contract is
|
|
12
|
+
unchanged.
|
|
11
13
|
|
|
12
14
|
## What it does
|
|
13
15
|
|
|
@@ -13,7 +13,7 @@ build-backend = "hatchling.build"
|
|
|
13
13
|
# Installs the locked runtime and development dependency sets.
|
|
14
14
|
# ============================================================================
|
|
15
15
|
name = "roadmap-cli"
|
|
16
|
-
version = "0.
|
|
16
|
+
version = "0.3.0"
|
|
17
17
|
description = "Git-native command line tool for local project roadmap management"
|
|
18
18
|
readme = "README.md"
|
|
19
19
|
requires-python = ">=3.12,<4.0"
|
|
@@ -61,7 +61,7 @@ dev = [
|
|
|
61
61
|
"pytest-cov>=7.1.0",
|
|
62
62
|
"pytest-asyncio>=1.3.0",
|
|
63
63
|
"pytest-aiohttp>=1.1.0",
|
|
64
|
-
"ruff==0.16.
|
|
64
|
+
"ruff==0.16.5",
|
|
65
65
|
"pre-commit>=4.6.0",
|
|
66
66
|
"bandit[toml]>=1.9.4",
|
|
67
67
|
"radon>=6.0.1,<7.0.0",
|
|
@@ -70,6 +70,7 @@ dev = [
|
|
|
70
70
|
"pytest-benchmark>=5.2.3",
|
|
71
71
|
"pyright>=1.1.411",
|
|
72
72
|
"pytest-mock>=3.15.1",
|
|
73
|
+
"ty>=0.0.77",
|
|
73
74
|
]
|
|
74
75
|
|
|
75
76
|
[project.urls]
|
|
@@ -112,10 +113,19 @@ include = [
|
|
|
112
113
|
# - ruff.toml: Linting and formatting rules
|
|
113
114
|
# - bandit.toml: Security scanning configuration
|
|
114
115
|
# - .coveragerc: Test coverage thresholds and exclusions
|
|
115
|
-
# - pyrightconfig.json: Type checking configuration
|
|
116
|
+
# - pyrightconfig.json: Type checking configuration (current)
|
|
116
117
|
|
|
117
|
-
#
|
|
118
|
-
#
|
|
118
|
+
# ty runs alongside Pyright and is enforced in CI and pre-commit (0.3 quality
|
|
119
|
+
# cleanup, see notes/path_to_1_point_0.md).
|
|
120
|
+
[tool.ty.src]
|
|
121
|
+
include = ["roadmap", "tests"]
|
|
122
|
+
# These fixture trees are synthetic "roadmap" packages used only for the
|
|
123
|
+
# architecture checker's AST scan; they are never actually imported, so
|
|
124
|
+
# ty's import resolution has nothing real to check them against.
|
|
125
|
+
exclude = ["tests/policy/fixtures/architecture"]
|
|
126
|
+
|
|
127
|
+
[tool.ty.environment]
|
|
128
|
+
python-version = "3.12"
|
|
119
129
|
|
|
120
130
|
[tool.pytest.ini_options]
|
|
121
131
|
testpaths = ["tests"]
|
|
@@ -4,10 +4,10 @@ from importlib.metadata import PackageNotFoundError, version
|
|
|
4
4
|
|
|
5
5
|
# Installed artifacts read their distribution metadata; source checkouts use the
|
|
6
6
|
# matching fallback without importing the executable adapter as a side effect.
|
|
7
|
-
__version__ = "0.
|
|
7
|
+
__version__ = "0.3.0"
|
|
8
8
|
try:
|
|
9
9
|
__version__ = version("roadmap-cli")
|
|
10
10
|
except PackageNotFoundError:
|
|
11
|
-
__version__ = "0.
|
|
11
|
+
__version__ = "0.3.0"
|
|
12
12
|
|
|
13
13
|
__all__ = ["__version__"]
|
|
@@ -96,21 +96,21 @@ class RoadmapClickGroup(click.Group):
|
|
|
96
96
|
with formatter.section("Commands"):
|
|
97
97
|
formatter.write_dl(rows)
|
|
98
98
|
|
|
99
|
-
def get_command(self, ctx: click.Context,
|
|
99
|
+
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
|
|
100
100
|
"""Load one registered command only when Click requests it."""
|
|
101
|
-
if (attached := super().get_command(ctx,
|
|
101
|
+
if (attached := super().get_command(ctx, cmd_name)) is not None:
|
|
102
102
|
return attached
|
|
103
|
-
if
|
|
104
|
-
return self._command_cache[
|
|
105
|
-
location = self._command_registry.get(
|
|
103
|
+
if cmd_name in self._command_cache:
|
|
104
|
+
return self._command_cache[cmd_name]
|
|
105
|
+
location = self._command_registry.get(cmd_name)
|
|
106
106
|
if location is None:
|
|
107
107
|
return None
|
|
108
108
|
module_path, attribute, _help = location
|
|
109
109
|
try:
|
|
110
|
-
self._command_cache[
|
|
110
|
+
self._command_cache[cmd_name] = command = getattr(importlib.import_module(module_path), attribute) # fmt: skip
|
|
111
111
|
except Exception as error:
|
|
112
112
|
self._console_factory().print(
|
|
113
|
-
f"⚠️ Failed to load command '{
|
|
113
|
+
f"⚠️ Failed to load command '{cmd_name}': {error}", style="yellow"
|
|
114
114
|
)
|
|
115
115
|
return None
|
|
116
116
|
return command
|
|
@@ -11,25 +11,40 @@ import click
|
|
|
11
11
|
from roadmap.adapters.inbound.cli.cli_command_helpers import require_initialized
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def
|
|
15
|
-
backups_dir: Path, keep: int, days: int | None, now: datetime
|
|
16
|
-
) -> tuple[Path, ...]:
|
|
14
|
+
def _validate_retention_options(keep: int, days: int | None) -> None:
|
|
17
15
|
if keep < 0:
|
|
18
16
|
raise click.BadParameter("keep must be non-negative", param_hint="--keep")
|
|
19
17
|
if days is not None and days < 0:
|
|
20
18
|
raise click.BadParameter("days must be non-negative", param_hint="--days")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _group_backups(backups_dir: Path) -> dict[str, list[Path]]:
|
|
21
22
|
grouped: dict[str, list[Path]] = defaultdict(list)
|
|
22
23
|
for path in backups_dir.glob("*.backup.md") if backups_dir.exists() else ():
|
|
23
24
|
parts = path.stem.split("_")
|
|
24
25
|
grouped["_".join(parts[:-1])].append(path)
|
|
26
|
+
return grouped
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _stale_paths(paths: list[Path], keep: int, cutoff: datetime | None) -> list[Path]:
|
|
30
|
+
ordered = sorted(paths, key=lambda path: (-path.stat().st_mtime, path.name))
|
|
31
|
+
stale: list[Path] = []
|
|
32
|
+
for index, path in enumerate(ordered):
|
|
33
|
+
modified = datetime.fromtimestamp(path.stat().st_mtime, tz=UTC)
|
|
34
|
+
if index >= keep or (cutoff is not None and modified < cutoff):
|
|
35
|
+
stale.append(path)
|
|
36
|
+
return stale
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _candidates(
|
|
40
|
+
backups_dir: Path, keep: int, days: int | None, now: datetime
|
|
41
|
+
) -> tuple[Path, ...]:
|
|
42
|
+
_validate_retention_options(keep, days)
|
|
43
|
+
grouped = _group_backups(backups_dir)
|
|
25
44
|
cutoff = now - timedelta(days=days) if days is not None else None
|
|
26
45
|
selected: list[Path] = []
|
|
27
46
|
for paths in grouped.values():
|
|
28
|
-
|
|
29
|
-
for index, path in enumerate(ordered):
|
|
30
|
-
modified = datetime.fromtimestamp(path.stat().st_mtime, tz=UTC)
|
|
31
|
-
if index >= keep or (cutoff is not None and modified < cutoff):
|
|
32
|
-
selected.append(path)
|
|
47
|
+
selected.extend(_stale_paths(paths, keep, cutoff))
|
|
33
48
|
return tuple(sorted(selected))
|
|
34
49
|
|
|
35
50
|
|
|
@@ -40,6 +55,39 @@ def _diagnostic_matches(core, finding_id: str) -> tuple[str, ...]:
|
|
|
40
55
|
)
|
|
41
56
|
|
|
42
57
|
|
|
58
|
+
def _selected_diagnostic_check(
|
|
59
|
+
check_folders: bool, check_duplicates: bool, check_malformed: bool
|
|
60
|
+
) -> str | None:
|
|
61
|
+
checks = {
|
|
62
|
+
"canonical.noncanonical-path": check_folders,
|
|
63
|
+
"canonical.duplicate-id": check_duplicates,
|
|
64
|
+
"canonical.invalid": check_malformed,
|
|
65
|
+
}
|
|
66
|
+
selected = [name for name, enabled in checks.items() if enabled]
|
|
67
|
+
if len(selected) > 1:
|
|
68
|
+
raise click.UsageError("select only one --check-* option")
|
|
69
|
+
return selected[0] if selected else None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _report_diagnostic_matches(core, finding_id: str) -> None:
|
|
73
|
+
matches = _diagnostic_matches(core, finding_id)
|
|
74
|
+
click.echo("\n".join(matches) + ("\n" if matches else "No findings.\n"), nl=False)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _remove_backups(candidates: tuple[Path, ...], relative: tuple[str, ...]) -> None:
|
|
78
|
+
failures: list[str] = []
|
|
79
|
+
for path, label in zip(candidates, relative, strict=True):
|
|
80
|
+
try:
|
|
81
|
+
path.unlink()
|
|
82
|
+
except OSError as error:
|
|
83
|
+
failures.append(f"{label}: {error}")
|
|
84
|
+
if failures:
|
|
85
|
+
raise click.ClickException(
|
|
86
|
+
"Backup cleanup was incomplete:\n" + "\n".join(failures)
|
|
87
|
+
)
|
|
88
|
+
click.echo(f"Removed {len(candidates)} backup file(s).")
|
|
89
|
+
|
|
90
|
+
|
|
43
91
|
@click.command()
|
|
44
92
|
@click.option("--keep", type=int, default=10, show_default=True)
|
|
45
93
|
@click.option("--days", type=int)
|
|
@@ -66,19 +114,11 @@ def cleanup(
|
|
|
66
114
|
) -> None:
|
|
67
115
|
"""Preview or remove only retention-qualified legacy backup files."""
|
|
68
116
|
del backups_only, verbose
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
selected_checks = [name for name, enabled in checks.items() if enabled]
|
|
75
|
-
if len(selected_checks) > 1:
|
|
76
|
-
raise click.UsageError("select only one --check-* option")
|
|
77
|
-
if selected_checks:
|
|
78
|
-
matches = _diagnostic_matches(ctx.obj["core"], selected_checks[0])
|
|
79
|
-
click.echo(
|
|
80
|
-
"\n".join(matches) + ("\n" if matches else "No findings.\n"), nl=False
|
|
81
|
-
)
|
|
117
|
+
diagnostic_check = _selected_diagnostic_check(
|
|
118
|
+
check_folders, check_duplicates, check_malformed
|
|
119
|
+
)
|
|
120
|
+
if diagnostic_check is not None:
|
|
121
|
+
_report_diagnostic_matches(ctx.obj["core"], diagnostic_check)
|
|
82
122
|
return
|
|
83
123
|
roadmap_dir = ctx.obj["core"].roadmap_dir
|
|
84
124
|
candidates = _candidates(roadmap_dir / "backups", keep, days, datetime.now(UTC))
|
|
@@ -94,14 +134,4 @@ def cleanup(
|
|
|
94
134
|
return
|
|
95
135
|
if not force:
|
|
96
136
|
click.confirm("Remove exactly these backup files?", abort=True, default=False)
|
|
97
|
-
|
|
98
|
-
for path, label in zip(candidates, relative, strict=True):
|
|
99
|
-
try:
|
|
100
|
-
path.unlink()
|
|
101
|
-
except OSError as error:
|
|
102
|
-
failures.append(f"{label}: {error}")
|
|
103
|
-
if failures:
|
|
104
|
-
raise click.ClickException(
|
|
105
|
-
"Backup cleanup was incomplete:\n" + "\n".join(failures)
|
|
106
|
-
)
|
|
107
|
-
click.echo(f"Removed {len(candidates)} backup file(s).")
|
|
137
|
+
_remove_backups(candidates, relative)
|
|
@@ -9,17 +9,67 @@ Provides decorators and functions to reduce duplication in CLI commands:
|
|
|
9
9
|
|
|
10
10
|
import functools
|
|
11
11
|
import sys
|
|
12
|
-
from collections.abc import Callable
|
|
12
|
+
from collections.abc import Callable, Sequence
|
|
13
13
|
from typing import Any, TypeVar
|
|
14
14
|
|
|
15
15
|
import click # type: ignore[import-not-found]
|
|
16
16
|
from rich.console import Console # type: ignore[import-not-found]
|
|
17
17
|
|
|
18
|
+
from roadmap.application.failures import ApplicationFailure
|
|
19
|
+
from roadmap.domain.failures import DomainFailure
|
|
20
|
+
|
|
18
21
|
console = Console()
|
|
19
22
|
|
|
20
23
|
F = TypeVar("F", bound=Callable[..., Any])
|
|
21
24
|
|
|
22
25
|
|
|
26
|
+
def invoke(operation: Callable[[], Any]) -> Any:
|
|
27
|
+
"""Translate stable application/domain failures into Click failures."""
|
|
28
|
+
try:
|
|
29
|
+
return operation()
|
|
30
|
+
except (ApplicationFailure, DomainFailure, ValueError) as error:
|
|
31
|
+
raise click.ClickException(str(error)) from error
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def projection_warning(result: Any) -> None:
|
|
35
|
+
if getattr(result, "projection_stale", False):
|
|
36
|
+
click.echo("Warning: SQLite projection is stale; canonical Markdown was saved.")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def echo_batch_result(
|
|
40
|
+
noun: str,
|
|
41
|
+
items: Sequence[Any],
|
|
42
|
+
dry_run: bool,
|
|
43
|
+
*,
|
|
44
|
+
action: str,
|
|
45
|
+
label: Callable[[Any], str] = lambda item: str(item.name),
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Echo the "<verb> <noun> <id>: <label>" summary shared by batch
|
|
48
|
+
archive/restore commands. ``action`` is the base verb (e.g. "archive",
|
|
49
|
+
"restore"); both currently used verbs conjugate regularly to their past
|
|
50
|
+
tense for the non-dry-run wording.
|
|
51
|
+
"""
|
|
52
|
+
past_tense = action + ("d" if action.endswith("e") else "ed")
|
|
53
|
+
verb = f"Would {action}" if dry_run else past_tense.capitalize()
|
|
54
|
+
for item in items:
|
|
55
|
+
click.echo(f"{verb} {noun} {item.id}: {label(item)}")
|
|
56
|
+
if not items:
|
|
57
|
+
click.echo(f"No matching {noun}s.")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def echo_archived_list(
|
|
61
|
+
noun: str,
|
|
62
|
+
values: Sequence[Any],
|
|
63
|
+
label: Callable[[Any], str] = lambda item: str(item.name),
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Echo the archived-entity listing shared by the "--list" branch of
|
|
66
|
+
the project/milestone archive commands."""
|
|
67
|
+
if not values:
|
|
68
|
+
click.echo(f"No archived {noun}s.")
|
|
69
|
+
for item in values:
|
|
70
|
+
click.echo(f"{item.id} {label(item)}")
|
|
71
|
+
|
|
72
|
+
|
|
23
73
|
def require_initialized(func: Callable) -> Callable:
|
|
24
74
|
"""Require roadmap to be initialized before command execution.
|
|
25
75
|
|
|
@@ -74,40 +74,62 @@ def export(
|
|
|
74
74
|
click.echo(f"Exported {len(records)} issues to {output}", err=True)
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
def
|
|
78
|
-
if value is None:
|
|
79
|
-
return IssueListQuery(scope=IssueScope.ALL)
|
|
77
|
+
def _validate_filter_string(value: str) -> tuple[str, str]:
|
|
80
78
|
if "=" not in value:
|
|
81
79
|
raise click.BadParameter("filter must use key=value", param_hint="--filter")
|
|
82
80
|
key, expected = (part.strip() for part in value.split("=", 1))
|
|
83
81
|
if not key or not expected:
|
|
84
82
|
raise click.BadParameter("filter must use key=value", param_hint="--filter")
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
param_hint="--filter",
|
|
104
|
-
) from error
|
|
105
|
-
raise click.BadParameter(
|
|
106
|
-
"supported filter keys are status, priority, issue_type, assignee, and retention",
|
|
107
|
-
param_hint="--filter",
|
|
83
|
+
return key, expected
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _retention_query(expected: str) -> IssueListQuery:
|
|
87
|
+
try:
|
|
88
|
+
return IssueListQuery(scope=IssueScope(expected))
|
|
89
|
+
except ValueError as error:
|
|
90
|
+
raise click.BadParameter(
|
|
91
|
+
"retention must be visible, closed, archived, or all",
|
|
92
|
+
param_hint="--filter",
|
|
93
|
+
) from error
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _status_query(expected: str) -> IssueListQuery:
|
|
97
|
+
return IssueListQuery(
|
|
98
|
+
scope=IssueScope.ALL,
|
|
99
|
+
open_only=expected == "open",
|
|
100
|
+
status=None if expected == "open" else expected,
|
|
108
101
|
)
|
|
109
102
|
|
|
110
103
|
|
|
104
|
+
_FILTER_BUILDERS = {
|
|
105
|
+
"status": _status_query,
|
|
106
|
+
"priority": lambda expected: IssueListQuery(
|
|
107
|
+
scope=IssueScope.ALL, priority=expected
|
|
108
|
+
),
|
|
109
|
+
"issue_type": lambda expected: IssueListQuery(
|
|
110
|
+
scope=IssueScope.ALL, issue_type=expected
|
|
111
|
+
),
|
|
112
|
+
"assignee": lambda expected: IssueListQuery(
|
|
113
|
+
scope=IssueScope.ALL, assignee=expected
|
|
114
|
+
),
|
|
115
|
+
"retention": _retention_query,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _query(value: str | None) -> IssueListQuery:
|
|
120
|
+
if value is None:
|
|
121
|
+
return IssueListQuery(scope=IssueScope.ALL)
|
|
122
|
+
key, expected = _validate_filter_string(value)
|
|
123
|
+
builder = _FILTER_BUILDERS.get(key)
|
|
124
|
+
if builder is None:
|
|
125
|
+
raise click.BadParameter(
|
|
126
|
+
"supported filter keys are status, priority, issue_type, assignee, "
|
|
127
|
+
"and retention",
|
|
128
|
+
param_hint="--filter",
|
|
129
|
+
)
|
|
130
|
+
return builder(expected)
|
|
131
|
+
|
|
132
|
+
|
|
111
133
|
def _timestamp(value: Any) -> str | None:
|
|
112
134
|
return None if value is None else value.value.isoformat()
|
|
113
135
|
|
|
@@ -157,38 +179,39 @@ def _issue_record(record: IssueQueryRecord) -> dict[str, Any]:
|
|
|
157
179
|
}
|
|
158
180
|
|
|
159
181
|
|
|
160
|
-
def
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
182
|
+
def _render_json(rows: list[dict[str, Any]]) -> str:
|
|
183
|
+
return (
|
|
184
|
+
json.dumps(
|
|
185
|
+
{
|
|
186
|
+
"schema_version": EXPORT_SCHEMA_VERSION,
|
|
187
|
+
"kind": "roadmap.issue-export",
|
|
188
|
+
"issues": rows,
|
|
189
|
+
},
|
|
190
|
+
ensure_ascii=False,
|
|
191
|
+
indent=2,
|
|
192
|
+
sort_keys=True,
|
|
193
|
+
)
|
|
194
|
+
+ "\n"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _render_csv(rows: list[dict[str, Any]]) -> str:
|
|
199
|
+
stream = io.StringIO(newline="")
|
|
200
|
+
writer = csv.DictWriter(stream, fieldnames=CSV_FIELDS, lineterminator="\n")
|
|
201
|
+
writer.writeheader()
|
|
202
|
+
for row in rows:
|
|
203
|
+
writer.writerow(
|
|
204
|
+
{
|
|
205
|
+
field: json.dumps(row[field], ensure_ascii=False, separators=(",", ":"))
|
|
206
|
+
if field in {"labels", "depends_on", "blocks"}
|
|
207
|
+
else row[field]
|
|
208
|
+
for field in CSV_FIELDS
|
|
209
|
+
}
|
|
175
210
|
)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
for row in rows:
|
|
181
|
-
writer.writerow(
|
|
182
|
-
{
|
|
183
|
-
field: json.dumps(
|
|
184
|
-
row[field], ensure_ascii=False, separators=(",", ":")
|
|
185
|
-
)
|
|
186
|
-
if field in {"labels", "depends_on", "blocks"}
|
|
187
|
-
else row[field]
|
|
188
|
-
for field in CSV_FIELDS
|
|
189
|
-
}
|
|
190
|
-
)
|
|
191
|
-
return stream.getvalue()
|
|
211
|
+
return stream.getvalue()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _render_markdown(rows: list[dict[str, Any]]) -> str:
|
|
192
215
|
lines = [
|
|
193
216
|
"---",
|
|
194
217
|
f"schema_version: {EXPORT_SCHEMA_VERSION}",
|
|
@@ -211,5 +234,13 @@ def _render(records: tuple[IssueQueryRecord, ...], format_name: str) -> str:
|
|
|
211
234
|
return "\n".join(lines) + "\n"
|
|
212
235
|
|
|
213
236
|
|
|
237
|
+
_RENDERERS = {"json": _render_json, "csv": _render_csv, "markdown": _render_markdown}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _render(records: tuple[IssueQueryRecord, ...], format_name: str) -> str:
|
|
241
|
+
rows = [_issue_record(record) for record in records]
|
|
242
|
+
return _RENDERERS[format_name](rows)
|
|
243
|
+
|
|
244
|
+
|
|
214
245
|
def _markdown(value: object) -> str:
|
|
215
246
|
return str(value).replace("|", "\\|").replace("\n", "<br>")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Centralized exception handling for CLI commands.
|
|
2
|
+
|
|
3
|
+
This module provides error handling and formatting for all exceptions
|
|
4
|
+
raised by CLI commands, ensuring consistent user-facing error messages
|
|
5
|
+
and proper stderr output with correct exit codes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from roadmap.adapters.inbound.cli.console import get_console_stderr
|
|
11
|
+
from roadmap.application.failures import ApplicationFailure
|
|
12
|
+
from roadmap.domain.failures import DomainFailure
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def handle_cli_exception(
|
|
16
|
+
ctx: click.Context, error: Exception, show_traceback: bool = False
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Centralized exception handler for CLI commands.
|
|
19
|
+
|
|
20
|
+
Catches RoadmapException instances and formats them for user output,
|
|
21
|
+
directing errors to stderr with proper exit codes. Other exceptions
|
|
22
|
+
are re-raised or logged as needed.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
ctx: Click context
|
|
26
|
+
error: The exception to handle
|
|
27
|
+
show_traceback: Whether to show full traceback (for debugging)
|
|
28
|
+
"""
|
|
29
|
+
stderr_console = get_console_stderr()
|
|
30
|
+
|
|
31
|
+
if isinstance(error, ApplicationFailure | DomainFailure | ValueError):
|
|
32
|
+
stderr_console.print(f"Error: {error}")
|
|
33
|
+
else:
|
|
34
|
+
stderr_console.print(f"Error: {error}")
|
|
35
|
+
if show_traceback:
|
|
36
|
+
stderr_console.print_exception()
|
|
37
|
+
ctx.exit(1)
|
|
@@ -62,7 +62,7 @@ def _render(
|
|
|
62
62
|
writer = csv.DictWriter(stream, fieldnames=fields, lineterminator="\n")
|
|
63
63
|
writer.writeheader()
|
|
64
64
|
if not summary_only:
|
|
65
|
-
writer.writerows(payload["findings"]) # type: ignore[arg-type]
|
|
65
|
+
writer.writerows(payload["findings"]) # type: ignore[arg-type, ty:invalid-argument-type]
|
|
66
66
|
return stream.getvalue()
|
|
67
67
|
lines = [f"Roadmap health: {payload['status']}"]
|
|
68
68
|
if summary_only:
|
|
@@ -79,6 +79,30 @@ def _render(
|
|
|
79
79
|
return "\n".join(lines) + "\n"
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def _filter_by_entity(findings: tuple, entities: tuple[str, ...]) -> tuple:
|
|
83
|
+
if not entities:
|
|
84
|
+
return findings
|
|
85
|
+
selected = {value.casefold() for value in entities}
|
|
86
|
+
return tuple(
|
|
87
|
+
item for item in findings if item.scope.split("/", 1)[0].rstrip("s") in selected
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _filter_by_severity(findings: tuple, severities: tuple[str, ...]) -> tuple:
|
|
92
|
+
if not severities:
|
|
93
|
+
return findings
|
|
94
|
+
selected = {value.casefold() for value in severities}
|
|
95
|
+
return tuple(item for item in findings if item.severity.value in selected)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _filter_dependencies(findings: tuple, *, dependencies: bool) -> tuple:
|
|
99
|
+
if dependencies:
|
|
100
|
+
return findings
|
|
101
|
+
return tuple(
|
|
102
|
+
item for item in findings if item.finding_id != "canonical.broken-reference"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
82
106
|
def _filtered(
|
|
83
107
|
report: HealthReport,
|
|
84
108
|
entities: tuple[str, ...] = (),
|
|
@@ -87,20 +111,9 @@ def _filtered(
|
|
|
87
111
|
dependencies: bool = True,
|
|
88
112
|
) -> HealthReport:
|
|
89
113
|
findings = report.findings
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
item
|
|
94
|
-
for item in findings
|
|
95
|
-
if item.scope.split("/", 1)[0].rstrip("s") in selected
|
|
96
|
-
)
|
|
97
|
-
if severities:
|
|
98
|
-
selected = {value.casefold() for value in severities}
|
|
99
|
-
findings = tuple(item for item in findings if item.severity.value in selected)
|
|
100
|
-
if not dependencies:
|
|
101
|
-
findings = tuple(
|
|
102
|
-
item for item in findings if item.finding_id != "canonical.broken-reference"
|
|
103
|
-
)
|
|
114
|
+
findings = _filter_by_entity(findings, entities)
|
|
115
|
+
findings = _filter_by_severity(findings, severities)
|
|
116
|
+
findings = _filter_dependencies(findings, dependencies=dependencies)
|
|
104
117
|
return HealthReport(findings)
|
|
105
118
|
|
|
106
119
|
|