specfact-cli 0.46.2__py3-none-any.whl → 0.46.4__py3-none-any.whl
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.
- specfact_cli/__init__.py +1 -1
- specfact_cli/analyzers/code_analyzer.py +7 -7
- specfact_cli/analyzers/graph_analyzer.py +33 -39
- specfact_cli/modules/module_registry/module-package.yaml +3 -3
- specfact_cli/modules/module_registry/src/commands.py +1 -1
- specfact_cli/registry/module_installer.py +46 -4
- specfact_cli/utils/optional_deps.py +12 -15
- specfact_cli/utils/project_artifact_write.py +7 -6
- {specfact_cli-0.46.2.dist-info → specfact_cli-0.46.4.dist-info}/METADATA +11 -10
- {specfact_cli-0.46.2.dist-info → specfact_cli-0.46.4.dist-info}/RECORD +13 -13
- {specfact_cli-0.46.2.dist-info → specfact_cli-0.46.4.dist-info}/WHEEL +0 -0
- {specfact_cli-0.46.2.dist-info → specfact_cli-0.46.4.dist-info}/entry_points.txt +0 -0
- {specfact_cli-0.46.2.dist-info → specfact_cli-0.46.4.dist-info}/licenses/LICENSE +0 -0
specfact_cli/__init__.py
CHANGED
|
@@ -507,16 +507,16 @@ class CodeAnalyzer:
|
|
|
507
507
|
}
|
|
508
508
|
)
|
|
509
509
|
|
|
510
|
-
# Dependency Graph Analysis (requires
|
|
511
|
-
|
|
510
|
+
# Dependency Graph Analysis (requires pycg and networkx)
|
|
511
|
+
pycg_available, _ = check_cli_tool_available("pycg")
|
|
512
512
|
networkx_available = check_python_package_available("networkx")
|
|
513
|
-
graph_enabled =
|
|
513
|
+
graph_enabled = pycg_available and networkx_available
|
|
514
514
|
graph_used = graph_enabled # Used if both dependencies are available
|
|
515
515
|
|
|
516
|
-
if not
|
|
517
|
-
reason = "
|
|
518
|
-
elif not
|
|
519
|
-
reason = "
|
|
516
|
+
if not pycg_available and not networkx_available:
|
|
517
|
+
reason = "pycg and networkx not installed (install: pip install pycg networkx)"
|
|
518
|
+
elif not pycg_available:
|
|
519
|
+
reason = "pycg not installed (install: pip install pycg)"
|
|
520
520
|
elif not networkx_available:
|
|
521
521
|
reason = "networkx not installed (install: pip install networkx)"
|
|
522
522
|
else:
|
|
@@ -3,6 +3,8 @@ Graph-based dependency and call graph analysis.
|
|
|
3
3
|
|
|
4
4
|
Enhances AST and Semgrep analysis with graph-based dependency tracking,
|
|
5
5
|
call graph extraction, and architecture visualization.
|
|
6
|
+
|
|
7
|
+
Call graph extraction uses pycg (MIT) via subprocess. pyan3 (GPL-2.0) removed.
|
|
6
8
|
"""
|
|
7
9
|
|
|
8
10
|
from __future__ import annotations
|
|
@@ -28,7 +30,7 @@ class GraphAnalyzer:
|
|
|
28
30
|
"""
|
|
29
31
|
Graph-based dependency and call graph analysis.
|
|
30
32
|
|
|
31
|
-
Uses
|
|
33
|
+
Uses pycg for call graphs, NetworkX for dependency graphs,
|
|
32
34
|
and provides graph-based insights to complement AST and Semgrep.
|
|
33
35
|
"""
|
|
34
36
|
|
|
@@ -55,7 +57,7 @@ class GraphAnalyzer:
|
|
|
55
57
|
@ensure(lambda result: isinstance(result, dict), "Must return dict")
|
|
56
58
|
def extract_call_graph(self, file_path: Path) -> dict[str, list[str]]:
|
|
57
59
|
"""
|
|
58
|
-
Extract call graph using
|
|
60
|
+
Extract call graph using pycg (MIT).
|
|
59
61
|
|
|
60
62
|
Args:
|
|
61
63
|
file_path: Path to Python file
|
|
@@ -63,75 +65,67 @@ class GraphAnalyzer:
|
|
|
63
65
|
Returns:
|
|
64
66
|
Dictionary mapping function names to list of called functions
|
|
65
67
|
"""
|
|
66
|
-
# Check if pyan3 is available using utility function
|
|
67
68
|
from specfact_cli.utils.optional_deps import check_cli_tool_available
|
|
68
69
|
|
|
69
|
-
is_available, _ = check_cli_tool_available("
|
|
70
|
+
is_available, _ = check_cli_tool_available("pycg")
|
|
70
71
|
if not is_available:
|
|
71
|
-
#
|
|
72
|
+
# pycg not available — return empty (graceful degradation)
|
|
72
73
|
return {}
|
|
73
74
|
|
|
74
|
-
# Run
|
|
75
|
-
with tempfile.NamedTemporaryFile(mode="w", suffix=".
|
|
76
|
-
|
|
75
|
+
# Run pycg to generate JSON call graph
|
|
76
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as json_file:
|
|
77
|
+
json_path = Path(json_file.name)
|
|
77
78
|
try:
|
|
78
79
|
result = subprocess.run(
|
|
79
|
-
["
|
|
80
|
-
stdout=dot_file,
|
|
80
|
+
["pycg", "--package", str(self.repo_path), str(file_path), "--output", str(json_path)],
|
|
81
81
|
stderr=subprocess.PIPE,
|
|
82
82
|
text=True,
|
|
83
|
-
timeout=15,
|
|
83
|
+
timeout=15,
|
|
84
84
|
)
|
|
85
85
|
|
|
86
86
|
if result.returncode == 0:
|
|
87
|
-
|
|
88
|
-
call_graph = self._parse_dot_file(dot_path)
|
|
87
|
+
call_graph = self._parse_pycg_json(json_path)
|
|
89
88
|
file_key = str(file_path.relative_to(self.repo_path))
|
|
90
89
|
self.call_graphs[file_key] = call_graph
|
|
91
90
|
return call_graph
|
|
92
91
|
finally:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
dot_path.unlink()
|
|
92
|
+
if json_path.exists():
|
|
93
|
+
json_path.unlink()
|
|
96
94
|
|
|
97
95
|
return {}
|
|
98
96
|
|
|
99
97
|
@beartype
|
|
100
|
-
@require(lambda
|
|
98
|
+
@require(lambda json_path: isinstance(json_path, Path), "JSON path must be Path")
|
|
101
99
|
@ensure(lambda result: isinstance(result, dict), "Must return dict")
|
|
102
|
-
def
|
|
100
|
+
def _parse_pycg_json(self, json_path: Path) -> dict[str, list[str]]:
|
|
103
101
|
"""
|
|
104
|
-
Parse
|
|
102
|
+
Parse pycg JSON output into caller → [callees] format.
|
|
103
|
+
|
|
104
|
+
PyCG's simple JSON format is an adjacency list: edge ``(src, dst)`` is
|
|
105
|
+
``dst`` in the list for key ``src`` (caller → callees). See PyCG README
|
|
106
|
+
"Simple JSON format".
|
|
105
107
|
|
|
106
108
|
Args:
|
|
107
|
-
|
|
109
|
+
json_path: Path to pycg JSON output file
|
|
108
110
|
|
|
109
111
|
Returns:
|
|
110
112
|
Dictionary mapping function names to list of called functions
|
|
111
113
|
"""
|
|
112
|
-
|
|
114
|
+
import json
|
|
113
115
|
|
|
114
|
-
if not
|
|
116
|
+
if not json_path.exists():
|
|
115
117
|
return {}
|
|
116
118
|
|
|
117
119
|
try:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
for match in matches:
|
|
127
|
-
caller = match.group(1)
|
|
128
|
-
callee = match.group(2)
|
|
129
|
-
# Filter out internal Python functions (start with __)
|
|
120
|
+
raw: dict[str, list[str]] = json.loads(json_path.read_text(encoding="utf-8"))
|
|
121
|
+
except (json.JSONDecodeError, UnicodeDecodeError, OSError):
|
|
122
|
+
return {}
|
|
123
|
+
|
|
124
|
+
call_graph: dict[str, list[str]] = defaultdict(list)
|
|
125
|
+
for caller, callees in raw.items():
|
|
126
|
+
for callee in callees:
|
|
130
127
|
if not caller.startswith("__") and not callee.startswith("__"):
|
|
131
128
|
call_graph[caller].append(callee)
|
|
132
|
-
except (UnicodeDecodeError, Exception):
|
|
133
|
-
# Skip if parsing fails
|
|
134
|
-
pass
|
|
135
129
|
|
|
136
130
|
return dict(call_graph)
|
|
137
131
|
|
|
@@ -209,7 +203,7 @@ class GraphAnalyzer:
|
|
|
209
203
|
wait_on_shutdown: bool,
|
|
210
204
|
progress_callback: Any | None,
|
|
211
205
|
) -> None:
|
|
212
|
-
"""Populate graph with edges derived from
|
|
206
|
+
"""Populate graph with edges derived from pycg call graphs (parallel phase 2)."""
|
|
213
207
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
214
208
|
|
|
215
209
|
loaded_contents = self._load_python_file_contents_index(python_files)
|
|
@@ -238,7 +232,7 @@ class GraphAnalyzer:
|
|
|
238
232
|
"""
|
|
239
233
|
Build comprehensive dependency graph using NetworkX.
|
|
240
234
|
|
|
241
|
-
Combines AST-based imports with
|
|
235
|
+
Combines AST-based imports with pycg call graphs for complete
|
|
242
236
|
dependency tracking.
|
|
243
237
|
|
|
244
238
|
Args:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name: module-registry
|
|
2
|
-
version: 0.1.
|
|
2
|
+
version: 0.1.20
|
|
3
3
|
commands:
|
|
4
4
|
- module
|
|
5
5
|
category: core
|
|
@@ -17,5 +17,5 @@ publisher:
|
|
|
17
17
|
description: 'Manage modules: search, list, show, install, and upgrade.'
|
|
18
18
|
license: Apache-2.0
|
|
19
19
|
integrity:
|
|
20
|
-
checksum: sha256:
|
|
21
|
-
signature:
|
|
20
|
+
checksum: sha256:a92afa757a54ee63b84ae4a5f5b232cb5dbddacfcb31fcde3abcdb4927eada8c
|
|
21
|
+
signature: jelDGPZyCLLpyzqH4+S2t7V9ICy/puYEzdUu/GX0oPiWgMZRg8aZlnECZGy+m+sHvS0XX3hPAXCSp3IJf6hHDg==
|
|
@@ -545,7 +545,7 @@ def _uninstall_marketplace_default(normalized: str) -> None:
|
|
|
545
545
|
)
|
|
546
546
|
raise typer.Exit(1)
|
|
547
547
|
try:
|
|
548
|
-
uninstall_module(normalized)
|
|
548
|
+
uninstall_module(normalized, confirm_user_scope=True)
|
|
549
549
|
except ValueError as exc:
|
|
550
550
|
console.print(f"[red]{exc}[/red]")
|
|
551
551
|
raise typer.Exit(1) from exc
|
|
@@ -33,12 +33,20 @@ from specfact_cli.registry.dependency_resolver import (
|
|
|
33
33
|
resolve_dependencies,
|
|
34
34
|
)
|
|
35
35
|
from specfact_cli.registry.marketplace_client import download_module
|
|
36
|
-
from specfact_cli.registry.module_discovery import
|
|
36
|
+
from specfact_cli.registry.module_discovery import (
|
|
37
|
+
MARKETPLACE_MODULES_ROOT as DISCOVERY_MARKETPLACE_MODULES_ROOT,
|
|
38
|
+
USER_MODULES_ROOT as DISCOVERY_USER_MODULES_ROOT,
|
|
39
|
+
discover_all_modules,
|
|
40
|
+
)
|
|
37
41
|
from specfact_cli.registry.module_security import assert_module_allowed, ensure_publisher_trusted
|
|
38
42
|
from specfact_cli.runtime import is_debug_mode
|
|
39
43
|
|
|
40
44
|
|
|
41
|
-
|
|
45
|
+
# Single source of truth for install/uninstall: re-export the canonical roots
|
|
46
|
+
# defined in module_discovery so discovery, install, and delete-safety stay in
|
|
47
|
+
# lockstep (see also docs/agent-rules/55-dependency-hygiene.md).
|
|
48
|
+
USER_MODULES_ROOT = DISCOVERY_USER_MODULES_ROOT
|
|
49
|
+
MARKETPLACE_MODULES_ROOT = DISCOVERY_MARKETPLACE_MODULES_ROOT
|
|
42
50
|
|
|
43
51
|
|
|
44
52
|
@beartype
|
|
@@ -66,7 +74,17 @@ class _BundleDepsInstallContext:
|
|
|
66
74
|
logger: logging.Logger
|
|
67
75
|
|
|
68
76
|
|
|
69
|
-
|
|
77
|
+
@beartype
|
|
78
|
+
def _path_is_under_user_modules_install_tree(module_dir: Path) -> bool:
|
|
79
|
+
"""True when *module_dir* resolves under :data:`USER_MODULES_ROOT` (``--scope user`` tree)."""
|
|
80
|
+
try:
|
|
81
|
+
resolved = module_dir.resolve()
|
|
82
|
+
root = USER_MODULES_ROOT.resolve()
|
|
83
|
+
except OSError:
|
|
84
|
+
return False
|
|
85
|
+
return resolved == root or root in resolved.parents
|
|
86
|
+
|
|
87
|
+
|
|
70
88
|
MODULE_DOWNLOAD_CACHE_ROOT = Path.home() / ".specfact" / "downloads" / "cache"
|
|
71
89
|
_IGNORED_MODULE_DIR_NAMES = {"__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache", "logs", "tests"}
|
|
72
90
|
_IGNORED_MODULE_FILE_SUFFIXES = {".pyc", ".pyo"}
|
|
@@ -958,8 +976,17 @@ def uninstall_module(
|
|
|
958
976
|
*,
|
|
959
977
|
install_root: Path | None = None,
|
|
960
978
|
source_map: dict[str, str] | None = None,
|
|
979
|
+
confirm_user_scope: bool = False,
|
|
961
980
|
) -> None:
|
|
962
|
-
"""Uninstall a marketplace module from
|
|
981
|
+
"""Uninstall a marketplace module from discovered install roots.
|
|
982
|
+
|
|
983
|
+
Deleting under :data:`USER_MODULES_ROOT` (``~/.specfact/modules``) is guarded: callers must pass
|
|
984
|
+
``confirm_user_scope=True`` (the ``specfact module uninstall`` CLI does this) or set environment
|
|
985
|
+
variable ``SPECFACT_CONFIRM_USER_SCOPE_UNINSTALL=1`` for explicit scripted removal. This prevents
|
|
986
|
+
accidental user-scope data loss from non-interactive or mistaken programmatic calls (for example
|
|
987
|
+
hooks or agents). Tests should pass ``install_root`` pointing at a temporary directory, or set the
|
|
988
|
+
env var for intentional user-root coverage.
|
|
989
|
+
"""
|
|
963
990
|
logger = get_bridge_logger(__name__)
|
|
964
991
|
|
|
965
992
|
if source_map is None:
|
|
@@ -980,10 +1007,25 @@ def uninstall_module(
|
|
|
980
1007
|
else:
|
|
981
1008
|
candidate_roots = [USER_MODULES_ROOT, MARKETPLACE_MODULES_ROOT]
|
|
982
1009
|
|
|
1010
|
+
env_confirms_user = os.environ.get("SPECFACT_CONFIRM_USER_SCOPE_UNINSTALL", "").strip().lower() in {
|
|
1011
|
+
"1",
|
|
1012
|
+
"true",
|
|
1013
|
+
"yes",
|
|
1014
|
+
"on",
|
|
1015
|
+
}
|
|
1016
|
+
|
|
983
1017
|
for root in candidate_roots:
|
|
984
1018
|
module_path = root / module_name
|
|
985
1019
|
if not module_path.exists():
|
|
986
1020
|
continue
|
|
1021
|
+
if _path_is_under_user_modules_install_tree(module_path) and not (confirm_user_scope or env_confirms_user):
|
|
1022
|
+
raise ValueError(
|
|
1023
|
+
"Refusing to remove a module under the canonical user install tree "
|
|
1024
|
+
f"({USER_MODULES_ROOT}) at {module_path!s} without explicit confirmation. "
|
|
1025
|
+
"User-scope modules must not be deleted by accident from library or hook code. "
|
|
1026
|
+
"Use the `specfact module uninstall` command, pass confirm_user_scope=True from a "
|
|
1027
|
+
"trusted caller, or set SPECFACT_CONFIRM_USER_SCOPE_UNINSTALL=1 for scripted uninstalls."
|
|
1028
|
+
)
|
|
987
1029
|
shutil.rmtree(module_path)
|
|
988
1030
|
logger.debug("Uninstalled module '%s' from '%s'", module_name, root)
|
|
989
1031
|
return
|
|
@@ -3,6 +3,9 @@ Utilities for checking optional dependencies.
|
|
|
3
3
|
|
|
4
4
|
This module provides functions to check if optional dependencies are installed
|
|
5
5
|
and available, enabling graceful degradation when they're not present.
|
|
6
|
+
|
|
7
|
+
Enhanced-analysis CLI tools: pycg (MIT), bandit (MIT), graphviz (MIT).
|
|
8
|
+
pyan3 (GPL-2.0), syft (wrong PyPI package), bearer (wrong PyPI package) removed.
|
|
6
9
|
"""
|
|
7
10
|
|
|
8
11
|
from __future__ import annotations
|
|
@@ -77,7 +80,7 @@ def check_cli_tool_available(
|
|
|
77
80
|
(where tools installed via pip are typically located).
|
|
78
81
|
|
|
79
82
|
Args:
|
|
80
|
-
tool_name: Name of the CLI tool (e.g., "
|
|
83
|
+
tool_name: Name of the CLI tool (e.g., "pycg", "bandit", "graphviz")
|
|
81
84
|
version_flag: Flag to check version (default: "--version")
|
|
82
85
|
timeout: Timeout in seconds (default: 5)
|
|
83
86
|
|
|
@@ -126,24 +129,18 @@ def check_enhanced_analysis_dependencies() -> dict[str, tuple[bool, str | None]]
|
|
|
126
129
|
"""
|
|
127
130
|
Check availability of all enhanced analysis optional dependencies.
|
|
128
131
|
|
|
129
|
-
Note: Currently only pyan3 is actually used in the codebase.
|
|
130
|
-
syft and bearer are planned but not yet implemented.
|
|
131
|
-
|
|
132
132
|
Returns:
|
|
133
133
|
Dictionary mapping dependency name to (is_available, error_message) tuple:
|
|
134
|
-
- "
|
|
135
|
-
- "
|
|
136
|
-
- "
|
|
137
|
-
- "graphviz": (bool, str | None) - Graph visualization (Python package, PLANNED, not yet used)
|
|
134
|
+
- "pycg": (bool, str | None) - Python call graph analysis (MIT; replaces GPL pyan3)
|
|
135
|
+
- "bandit": (bool, str | None) - SAST security scanner (MIT)
|
|
136
|
+
- "graphviz": (bool, str | None) - Graph visualization (Python package)
|
|
138
137
|
"""
|
|
139
138
|
results: dict[str, tuple[bool, str | None]] = {}
|
|
140
139
|
|
|
141
|
-
#
|
|
142
|
-
results["
|
|
143
|
-
#
|
|
144
|
-
|
|
145
|
-
results["syft"] = check_cli_tool_available("syft")
|
|
146
|
-
results["bearer"] = check_cli_tool_available("bearer")
|
|
140
|
+
# pycg: MIT-licensed call graph tool (replaces pyan3 which was GPL-2.0)
|
|
141
|
+
results["pycg"] = check_cli_tool_available("pycg")
|
|
142
|
+
# bandit: MIT-licensed SAST scanner (replaces bearer which was the wrong PyPI package)
|
|
143
|
+
results["bandit"] = check_cli_tool_available("bandit")
|
|
147
144
|
|
|
148
145
|
# Check Python packages
|
|
149
146
|
graphviz_available = check_python_package_available("graphviz")
|
|
@@ -169,7 +166,7 @@ def get_enhanced_analysis_installation_hint() -> str:
|
|
|
169
166
|
pip install specfact-cli[enhanced-analysis]
|
|
170
167
|
|
|
171
168
|
Or install individually:
|
|
172
|
-
pip install
|
|
169
|
+
pip install pycg bandit graphviz
|
|
173
170
|
|
|
174
171
|
Note: graphviz also requires the system Graphviz library:
|
|
175
172
|
- Ubuntu/Debian: sudo apt-get install graphviz
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import re
|
|
6
7
|
import shutil
|
|
7
8
|
from dataclasses import dataclass
|
|
@@ -10,7 +11,7 @@ from enum import StrEnum
|
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from typing import Any, Final, cast
|
|
12
13
|
|
|
13
|
-
import
|
|
14
|
+
import commentjson
|
|
14
15
|
from beartype import beartype
|
|
15
16
|
from icontract import ensure, require
|
|
16
17
|
|
|
@@ -80,7 +81,7 @@ def _ordered_unique_strings(items: list[str]) -> list[str]:
|
|
|
80
81
|
|
|
81
82
|
def _write_new_vscode_settings_file(settings_path: Path, prompt_files: list[str]) -> None:
|
|
82
83
|
payload: dict[str, Any] = {"chat": {"promptFilesRecommendations": list(prompt_files)}}
|
|
83
|
-
text =
|
|
84
|
+
text = json.dumps(payload, indent=4) + "\n"
|
|
84
85
|
settings_path.write_text(text, encoding="utf-8")
|
|
85
86
|
|
|
86
87
|
|
|
@@ -103,7 +104,7 @@ def _load_root_dict_from_settings_text(
|
|
|
103
104
|
) -> tuple[dict[str, Any], Path | None]:
|
|
104
105
|
out_backup = backup_path
|
|
105
106
|
try:
|
|
106
|
-
loaded =
|
|
107
|
+
loaded = commentjson.loads(raw_text)
|
|
107
108
|
except ValueError as exc:
|
|
108
109
|
if not explicit_replace_unparseable:
|
|
109
110
|
raise StructuredJsonDocumentError(
|
|
@@ -212,8 +213,8 @@ def merge_vscode_settings_prompt_recommendations(
|
|
|
212
213
|
unusable ``chat`` / ``promptFilesRecommendations`` shape, raises ``StructuredJsonDocumentError``
|
|
213
214
|
unless ``explicit_replace_unparseable`` is True (backup, then recoverable rewrite).
|
|
214
215
|
|
|
215
|
-
Parses with
|
|
216
|
-
preserving original comment text or formatting
|
|
216
|
+
Parses with commentjson (strips ``//`` and ``/* */`` comments and trailing commas via MIT library).
|
|
217
|
+
Serialized output is canonical JSON without preserving original comment text or formatting.
|
|
217
218
|
"""
|
|
218
219
|
repo_root = repo_path.resolve()
|
|
219
220
|
settings_path = (repo_path / settings_relative).resolve()
|
|
@@ -249,6 +250,6 @@ def merge_vscode_settings_prompt_recommendations(
|
|
|
249
250
|
prompt_files=tuple(prompt_files),
|
|
250
251
|
),
|
|
251
252
|
)
|
|
252
|
-
out_text =
|
|
253
|
+
out_text = json.dumps(loaded, indent=4) + "\n"
|
|
253
254
|
settings_path.write_text(out_text, encoding="utf-8")
|
|
254
255
|
return settings_path
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: specfact-cli
|
|
3
|
-
Version: 0.46.
|
|
3
|
+
Version: 0.46.4
|
|
4
4
|
Summary: The swiss knife CLI for agile DevOps teams. Keep backlog, specs, tests, and code in sync with validation and contract enforcement for new projects and long-lived codebases.
|
|
5
5
|
Project-URL: Homepage, https://github.com/nold-ai/specfact-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/nold-ai/specfact-cli.git
|
|
@@ -224,12 +224,12 @@ Classifier: Topic :: Software Development :: Testing
|
|
|
224
224
|
Requires-Python: >=3.11
|
|
225
225
|
Requires-Dist: azure-identity>=1.17.1
|
|
226
226
|
Requires-Dist: beartype>=0.22.4
|
|
227
|
+
Requires-Dist: commentjson>=0.9.0
|
|
227
228
|
Requires-Dist: cryptography>=43.0.0
|
|
228
229
|
Requires-Dist: gitpython>=3.1.45
|
|
229
230
|
Requires-Dist: graphviz>=0.20.1
|
|
230
231
|
Requires-Dist: icontract>=2.7.1
|
|
231
232
|
Requires-Dist: jinja2>=3.1.6
|
|
232
|
-
Requires-Dist: json5>=0.9.28
|
|
233
233
|
Requires-Dist: jsonschema>=4.23.0
|
|
234
234
|
Requires-Dist: networkx>=3.4.2
|
|
235
235
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27.0
|
|
@@ -248,16 +248,18 @@ Provides-Extra: contracts
|
|
|
248
248
|
Requires-Dist: crosshair-tool>=0.0.97; extra == 'contracts'
|
|
249
249
|
Requires-Dist: hypothesis>=6.142.4; extra == 'contracts'
|
|
250
250
|
Provides-Extra: dev
|
|
251
|
+
Requires-Dist: bandit>=1.7.0; extra == 'dev'
|
|
251
252
|
Requires-Dist: basedpyright>=1.32.1; extra == 'dev'
|
|
252
|
-
Requires-Dist: bearer>=3.1.0; extra == 'dev'
|
|
253
253
|
Requires-Dist: beartype>=0.22.4; extra == 'dev'
|
|
254
254
|
Requires-Dist: crosshair-tool>=0.0.97; extra == 'dev'
|
|
255
255
|
Requires-Dist: graphviz>=0.20.1; extra == 'dev'
|
|
256
256
|
Requires-Dist: hypothesis>=6.142.4; extra == 'dev'
|
|
257
257
|
Requires-Dist: icontract>=2.7.1; extra == 'dev'
|
|
258
258
|
Requires-Dist: isort>=7.0.0; extra == 'dev'
|
|
259
|
+
Requires-Dist: pip-audit>=2.0.0; extra == 'dev'
|
|
260
|
+
Requires-Dist: pip-licenses>=4.0.0; extra == 'dev'
|
|
259
261
|
Requires-Dist: pip-tools>=7.5.1; extra == 'dev'
|
|
260
|
-
Requires-Dist:
|
|
262
|
+
Requires-Dist: pycg==0.0.7; extra == 'dev'
|
|
261
263
|
Requires-Dist: pylint>=4.0.2; extra == 'dev'
|
|
262
264
|
Requires-Dist: pytest-asyncio>=1.2.0; extra == 'dev'
|
|
263
265
|
Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
|
|
@@ -271,10 +273,8 @@ Requires-Dist: setuptools<82,>=69.0.0; extra == 'dev'
|
|
|
271
273
|
Requires-Dist: tomlkit>=0.13.3; extra == 'dev'
|
|
272
274
|
Requires-Dist: types-pyyaml>=6.0.12.20250516; extra == 'dev'
|
|
273
275
|
Provides-Extra: enhanced-analysis
|
|
274
|
-
Requires-Dist: bearer>=3.1.0; extra == 'enhanced-analysis'
|
|
275
276
|
Requires-Dist: graphviz>=0.20.1; extra == 'enhanced-analysis'
|
|
276
|
-
Requires-Dist:
|
|
277
|
-
Requires-Dist: syft>=0.9.5; extra == 'enhanced-analysis'
|
|
277
|
+
Requires-Dist: pycg==0.0.7; extra == 'enhanced-analysis'
|
|
278
278
|
Provides-Extra: scanning
|
|
279
279
|
Requires-Dist: semgrep>=1.144.0; extra == 'scanning'
|
|
280
280
|
Description-Content-Type: text/markdown
|
|
@@ -308,7 +308,7 @@ uvx specfact-cli code review run --path . --scope full
|
|
|
308
308
|
**Sample output:**
|
|
309
309
|
|
|
310
310
|
```text
|
|
311
|
-
SpecFact CLI - v0.46.
|
|
311
|
+
SpecFact CLI - v0.46.4
|
|
312
312
|
|
|
313
313
|
Running Ruff checks...
|
|
314
314
|
Running Radon complexity checks...
|
|
@@ -367,14 +367,15 @@ This repository uses a **modular** local hook layout (parity with `specfact-cli-
|
|
|
367
367
|
separate verify / format / YAML / Markdown / workflow / lint / Block 2 hooks). If you copy
|
|
368
368
|
[`.pre-commit-config.yaml`](.pre-commit-config.yaml) into another repo, you must also vendor the
|
|
369
369
|
referenced `scripts/*.sh` entrypoints (at minimum `scripts/pre-commit-quality-checks.sh`,
|
|
370
|
-
`scripts/pre-commit-verify-modules.sh`,
|
|
370
|
+
`scripts/pre-commit-verify-modules.sh`, `scripts/module-verify-policy.sh`, and
|
|
371
|
+
`scripts/git-branch-module-signature-flag.sh`) so hook
|
|
371
372
|
`entry:` paths resolve. Alternatively, skip vendoring the modular file and use the remote hook below.
|
|
372
373
|
|
|
373
374
|
For a **single-hook** setup in downstream repos, keep using the stable id and script shim:
|
|
374
375
|
|
|
375
376
|
```yaml
|
|
376
377
|
- repo: https://github.com/nold-ai/specfact-cli
|
|
377
|
-
rev: v0.46.
|
|
378
|
+
rev: v0.46.4
|
|
378
379
|
hooks:
|
|
379
380
|
- id: specfact-smart-checks
|
|
380
381
|
```
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
specfact_cli/__init__.py,sha256=
|
|
1
|
+
specfact_cli/__init__.py,sha256=l87i_0NORlszKunQixcew2HQdb8tkv3WRKOvjs_ZlY8,1504
|
|
2
2
|
specfact_cli/__main__.py,sha256=EpP5xlutNYI5AuMlMw8fGt1TywZUR1-CXHpzpWKyNuo,141
|
|
3
3
|
specfact_cli/cli.py,sha256=Td4-LRNKfB0IuUhpd5HV4EVh55bzhM_6krXsGZbU7nY,39395
|
|
4
4
|
specfact_cli/runtime.py,sha256=4tEcOsz2bE6-KuZF5nuHvHuwinO7l8fNAGzrra9pNrc,13474
|
|
@@ -20,11 +20,11 @@ specfact_cli/agents/registry.py,sha256=NE2WzNUpgNDAhl5BIqxVoAPyjLALdcf13Jwddah7i
|
|
|
20
20
|
specfact_cli/agents/sync_agent.py,sha256=AWp0k5AGaaKJvhpQ7rss3vCxG78hYRgskNU1LBfNABo,4351
|
|
21
21
|
specfact_cli/analyzers/__init__.py,sha256=p-fms9MQ_euOMklUXNBk_LH4htlWSWldkFvXF1jreMw,357
|
|
22
22
|
specfact_cli/analyzers/ambiguity_scanner.py,sha256=1rxDPjf56C4MrypJWutkXhbV1lS7uN0m7UHo--LCM8c,40334
|
|
23
|
-
specfact_cli/analyzers/code_analyzer.py,sha256=
|
|
23
|
+
specfact_cli/analyzers/code_analyzer.py,sha256=0q496rD1a3SfNuY9vTplUJ29-ZUbNSogAAxyDIDZLSg,83119
|
|
24
24
|
specfact_cli/analyzers/constitution_evidence_extractor.py,sha256=gkgSFfkDsQ3RtXFkTudz30XwUZ3qrhhg6ph256nb_-s,22110
|
|
25
25
|
specfact_cli/analyzers/contract_extractor.py,sha256=MuuMCwsHbKnLZRQvihnxs2-Y5f2za6IxbugwEVJDuNQ,16895
|
|
26
26
|
specfact_cli/analyzers/control_flow_analyzer.py,sha256=4xHFcdgrASFPDeLUEkSULii3ylRPdl__09ER3t2hrc0,10930
|
|
27
|
-
specfact_cli/analyzers/graph_analyzer.py,sha256=
|
|
27
|
+
specfact_cli/analyzers/graph_analyzer.py,sha256=zVTANf_v7jHYffXQBpn21sCwH4DADshlNAHfX13NLHc,18808
|
|
28
28
|
specfact_cli/analyzers/relationship_mapper.py,sha256=ZWymy5bkMp_chSBjh4dKMObjki8KyqYsWXLoDbg0HMs,16800
|
|
29
29
|
specfact_cli/analyzers/requirement_extractor.py,sha256=GZmvHgdkyOXbIT-FZiEmGJVcO30VSNAGKWRWSmspI60,11378
|
|
30
30
|
specfact_cli/analyzers/test_pattern_extractor.py,sha256=4BUSrk9Xe7DA6HMTsK26tvhCkhifVdOtw8yhT6SkBNI,14621
|
|
@@ -124,10 +124,10 @@ specfact_cli/modules/init/src/__init__.py,sha256=hCfRT0VH7kl70Rm_jHlNFlCLerJoI8I
|
|
|
124
124
|
specfact_cli/modules/init/src/app.py,sha256=laUNKmpNqrFl1Jab3bBYICAaq3GUsj5hN5yjVFYAjvE,107
|
|
125
125
|
specfact_cli/modules/init/src/commands.py,sha256=DQPQGXoCBWowa6ivhjuhTSMT9wj_0BmLKJzwpOqNYPk,28519
|
|
126
126
|
specfact_cli/modules/init/src/first_run_selection.py,sha256=nt7TS9gbQzUoSv8UXipunIo6A3IZS3JQWidyjDdvci4,12729
|
|
127
|
-
specfact_cli/modules/module_registry/module-package.yaml,sha256=
|
|
127
|
+
specfact_cli/modules/module_registry/module-package.yaml,sha256=fysoEfp4SLjw0qG9Le989VQxotdTwenzxFNChVSRUhI,699
|
|
128
128
|
specfact_cli/modules/module_registry/src/__init__.py,sha256=Y97pOZc6y1DLxUBSHeh15AQ-sWCrg67ESyUNI5NRShY,42
|
|
129
129
|
specfact_cli/modules/module_registry/src/app.py,sha256=sAKRXfaJNVM22rA952DFpG-12c8dazEb_zBQuc1KCF0,351
|
|
130
|
-
specfact_cli/modules/module_registry/src/commands.py,sha256=
|
|
130
|
+
specfact_cli/modules/module_registry/src/commands.py,sha256=cIDGZJWZvEP0Xyj7EAfptaczgK0zyWqPUGihyjOvul8,54746
|
|
131
131
|
specfact_cli/modules/upgrade/module-package.yaml,sha256=K0_ngKhlMaPpve8UTvuzXrvaA1zWBk-te30ygmkr28Q,646
|
|
132
132
|
specfact_cli/modules/upgrade/src/__init__.py,sha256=hCfRT0VH7kl70Rm_jHlNFlCLerJoI8InHi7OS74WIL8,39
|
|
133
133
|
specfact_cli/modules/upgrade/src/app.py,sha256=lOeaPyWumUuuWbmOTZB7ZbdmvKjDzma0B8fTU976Z00,113
|
|
@@ -147,7 +147,7 @@ specfact_cli/registry/marketplace_client.py,sha256=BH1WKr80UccV4VX-N2sL0IxQIC_Et
|
|
|
147
147
|
specfact_cli/registry/metadata.py,sha256=mSIW8UZgBzBdiAH_52K8GL-VBMgZoyGQXIXg0bsl1Oc,806
|
|
148
148
|
specfact_cli/registry/module_discovery.py,sha256=YljgvmHTULV8JHht1RpaCXyInvbMb675p0C8ilJYWRU,6183
|
|
149
149
|
specfact_cli/registry/module_grouping.py,sha256=Qly2YKxlsn1Wt9hDRlHMMZeui85w3KbbT86VAJ4Mz2o,3186
|
|
150
|
-
specfact_cli/registry/module_installer.py,sha256=
|
|
150
|
+
specfact_cli/registry/module_installer.py,sha256=oKIvuW5Fngf4xZNZkoR5dzB4JwyznY5AR2pYE87bRk4,41946
|
|
151
151
|
specfact_cli/registry/module_lifecycle.py,sha256=17QhSER6lQV4JeTn3rvF6lsFRTIjT_UrIzMGpVveFEw,9541
|
|
152
152
|
specfact_cli/registry/module_packages.py,sha256=xXlpeN31_Nj-jFhj4dWbIaRrPWaOvwOkyTrBp2MpSWU,56850
|
|
153
153
|
specfact_cli/registry/module_security.py,sha256=RfKJmkR9sdBXP7q1YBsK1Io-ys0vhHshhPR06uk5TjM,4517
|
|
@@ -204,12 +204,12 @@ specfact_cli/utils/icontract_helpers.py,sha256=tbT-Qsh6vxy4eQidf1iGPxVMgkSGtm4z6
|
|
|
204
204
|
specfact_cli/utils/ide_setup.py,sha256=wpZhm7J73wvm5fGLEMnC49DjL2LACv8PDQVE869_UVE,42210
|
|
205
205
|
specfact_cli/utils/incremental_check.py,sha256=jpU7l-doutVswUNIgsy9U-reJLQdSWjj1KASZZH0XTU,17333
|
|
206
206
|
specfact_cli/utils/metadata.py,sha256=K4h6Udk3_ZO6u3BxRhioeo4gBLeIGVS7BdsfDS5nEdw,5302
|
|
207
|
-
specfact_cli/utils/optional_deps.py,sha256=
|
|
207
|
+
specfact_cli/utils/optional_deps.py,sha256=uZ_8f6F1Mc_c-S1NLVDiuukMqATxhBWTUxRVmmvpdfA,6202
|
|
208
208
|
specfact_cli/utils/performance.py,sha256=HL6UJc5kSqosVTKfUXbFw0o4LY-rZHH-pq75VTjKQ-M,7675
|
|
209
209
|
specfact_cli/utils/persona_ownership.py,sha256=BUzUbYOXXOI5ROa9I0xWvDWujoW4jDMarGomzcWfA-U,1553
|
|
210
210
|
specfact_cli/utils/progress.py,sha256=AyYOsSjm6hAm_wo-pXvfx2Bq8P1d7geyaiDsZ2r8Z04,8670
|
|
211
211
|
specfact_cli/utils/progressive_disclosure.py,sha256=LMSPCogHL_vm1gl1Zr3zgOMuJV2-hWX9vjZMU1y8uUY,12944
|
|
212
|
-
specfact_cli/utils/project_artifact_write.py,sha256=
|
|
212
|
+
specfact_cli/utils/project_artifact_write.py,sha256=SqdHQORKxqA54UdUlW0-9CWiququYE0JUQdQh_YLB7c,9564
|
|
213
213
|
specfact_cli/utils/prompts.py,sha256=-Ah8WMQ87EX5LgB9sWhPCLHwbDrGqCFC-ujDyRo4JmU,6319
|
|
214
214
|
specfact_cli/utils/sdd_discovery.py,sha256=LHYdldSprtOEDEXJ1myg_5JGRWIwaitk3pHKi2IT9n8,7939
|
|
215
215
|
specfact_cli/utils/source_scanner.py,sha256=VmyJE8gvViGR124TuQjODr_0mBlV9kJxE14t0PjQwgE,24928
|
|
@@ -286,8 +286,8 @@ specfact_cli/resources/templates/policies/kanban.yaml,sha256=9jt7YYfZ8e8aB5skY2D
|
|
|
286
286
|
specfact_cli/resources/templates/policies/mixed.yaml,sha256=-fkrT-TLKLTBzjKeKr8bEJHFC-nh1TlyLOEFfgRdSi8,243
|
|
287
287
|
specfact_cli/resources/templates/policies/safe.yaml,sha256=uiwe5_ABvaU4nmq6rsLUn8zRotkBITItPUsdPZPTHTw,157
|
|
288
288
|
specfact_cli/resources/templates/policies/scrum.yaml,sha256=6FHYOntkjCxHAn-ga14IQzA1PvYUJgxfLsEQYtWJILM,188
|
|
289
|
-
specfact_cli-0.46.
|
|
290
|
-
specfact_cli-0.46.
|
|
291
|
-
specfact_cli-0.46.
|
|
292
|
-
specfact_cli-0.46.
|
|
293
|
-
specfact_cli-0.46.
|
|
289
|
+
specfact_cli-0.46.4.dist-info/METADATA,sha256=1VWUlv4JclZo8U9gAFdVQX1pisvC4d1iqQf9VlpfQL0,24060
|
|
290
|
+
specfact_cli-0.46.4.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
291
|
+
specfact_cli-0.46.4.dist-info/entry_points.txt,sha256=pwDD5Ttu6AF3p3T05M4G0aR2BELFyUu3214kjMRGEow,96
|
|
292
|
+
specfact_cli-0.46.4.dist-info/licenses/LICENSE,sha256=nWX_6oozyEFlF3mDaSWyBD9HLpyLRmCYYqdcgoVOrOk,11361
|
|
293
|
+
specfact_cli-0.46.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|