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 CHANGED
@@ -45,6 +45,6 @@ def _bootstrap_bundle_paths() -> None:
45
45
 
46
46
  _bootstrap_bundle_paths()
47
47
 
48
- __version__ = "0.46.2"
48
+ __version__ = "0.46.4"
49
49
 
50
50
  __all__ = ["__version__"]
@@ -507,16 +507,16 @@ class CodeAnalyzer:
507
507
  }
508
508
  )
509
509
 
510
- # Dependency Graph Analysis (requires pyan3 and networkx)
511
- pyan3_available, _ = check_cli_tool_available("pyan3")
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 = pyan3_available and networkx_available
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 pyan3_available and not networkx_available:
517
- reason = "pyan3 and networkx not installed (install: pip install pyan3 networkx)"
518
- elif not pyan3_available:
519
- reason = "pyan3 not installed (install: pip install pyan3)"
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 pyan for call graphs, NetworkX for dependency graphs,
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 pyan.
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("pyan3")
70
+ is_available, _ = check_cli_tool_available("pycg")
70
71
  if not is_available:
71
- # pyan3 not available, return empty
72
+ # pycg not available return empty (graceful degradation)
72
73
  return {}
73
74
 
74
- # Run pyan to generate DOT file
75
- with tempfile.NamedTemporaryFile(mode="w", suffix=".dot", delete=False) as dot_file:
76
- dot_path = Path(dot_file.name)
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
- ["pyan3", str(file_path), "--dot", "--no-defines", "--uses", "--defines"],
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, # Reduced from 30 to 15 seconds for faster processing
83
+ timeout=15,
84
84
  )
85
85
 
86
86
  if result.returncode == 0:
87
- # Parse DOT file to extract call relationships
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
- # Clean up temp file
94
- if dot_path.exists():
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 dot_path: isinstance(dot_path, Path), "DOT path must be Path")
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 _parse_dot_file(self, dot_path: Path) -> dict[str, list[str]]:
100
+ def _parse_pycg_json(self, json_path: Path) -> dict[str, list[str]]:
103
101
  """
104
- Parse DOT file to extract call graph.
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
- dot_path: Path to DOT file
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
- call_graph: dict[str, list[str]] = defaultdict(list)
114
+ import json
113
115
 
114
- if not dot_path.exists():
116
+ if not json_path.exists():
115
117
  return {}
116
118
 
117
119
  try:
118
- content = dot_path.read_text(encoding="utf-8")
119
- # Parse DOT format: "function_a" -> "function_b"
120
- import re
121
-
122
- # Pattern: "function_a" -> "function_b"
123
- edge_pattern = r'"([^"]+)"\s*->\s*"([^"]+)"'
124
- matches = re.finditer(edge_pattern, content)
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 pyan call graphs (parallel phase 2)."""
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 pyan call graphs for complete
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.18
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:913da1a90a94691366c71fc23e91cfc57c38ffb97e4ea739229fc9897cc91131
21
- signature: lclyeM5FB+AYl+VKScUXBi4+lBC8vSdXE7ki6L/m3C7TrW81x60It50jhcP0+VL3xlPPIPihIQ8KCh2NfWWVBg==
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 discover_all_modules
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
- USER_MODULES_ROOT = Path.home() / ".specfact" / "modules"
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
- MARKETPLACE_MODULES_ROOT = Path.home() / ".specfact" / "marketplace-modules"
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 the local canonical user root."""
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., "pyan3", "syft", "bearer")
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
- - "pyan3": (bool, str | None) - Python call graph analysis (USED)
135
- - "syft": (bool, str | None) - SBOM generation (PLANNED, not yet used)
136
- - "bearer": (bool, str | None) - Data flow analysis (PLANNED, not yet used)
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
- # Check CLI tools
142
- results["pyan3"] = check_cli_tool_available("pyan3")
143
- # Note: syft and bearer are checked but not yet used in the codebase
144
- # They are included here for future use when SBOM and data flow analysis are implemented
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 pyan3 syft bearer graphviz
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 json5
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 = json5.dumps(payload, indent=4, quote_keys=True, trailing_commas=False) + "\n"
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 = json5.loads(raw_text)
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 JSON5 (comments and trailing commas). Serialized output is canonical JSON5/JSON without
216
- preserving original comment text or formatting from the input file.
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 = json5.dumps(loaded, indent=4, quote_keys=True, trailing_commas=False) + "\n"
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.2
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: pyan3>=1.2.0; extra == 'dev'
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: pyan3>=1.2.0; extra == 'enhanced-analysis'
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.1
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`, and `scripts/git-branch-module-signature-flag.sh`) so hook
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.1
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=vp1QTe54XWLmrxIQvJDKCgQbraaS5N7ufHPU2kqaDEc,1504
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=sThm9DGMotjIDxvPLCSlYikVi154c21Oirw3syKPrGc,83129
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=yI1auOMmLZuP5T1L8jsbxyLr8Bj5yjtQerstLPya848,19012
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=GDXxk9t_kUDfi7-BS8clunyfZvw9df8zMF5dYROqIKU,699
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=973tmw7lsAgtFiWp7Kin2oarb_bRSqSu7wS6NRS7fw8,54721
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=U_Sk7MGvZ3TiPOUxsAKs2QhBtRqHIjkb0fal1JMzFFs,39841
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=WCwKt_v3HOtu-vdY2oLlxgyYm5b1n5ZrYnDzSSmv3Js,6352
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=9WO9apH1RvCzjEtkLteJbax2n1bQTr3bTUhOJahfhiQ,9598
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.2.dist-info/METADATA,sha256=lmUwYav26-wEeClaxp80SRhQQ8UHofGk1vl3c4hOx1w,24039
290
- specfact_cli-0.46.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
291
- specfact_cli-0.46.2.dist-info/entry_points.txt,sha256=pwDD5Ttu6AF3p3T05M4G0aR2BELFyUu3214kjMRGEow,96
292
- specfact_cli-0.46.2.dist-info/licenses/LICENSE,sha256=nWX_6oozyEFlF3mDaSWyBD9HLpyLRmCYYqdcgoVOrOk,11361
293
- specfact_cli-0.46.2.dist-info/RECORD,,
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,,