specfact-cli 0.4.2__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.
Files changed (62) hide show
  1. specfact_cli/__init__.py +14 -0
  2. specfact_cli/agents/__init__.py +24 -0
  3. specfact_cli/agents/analyze_agent.py +392 -0
  4. specfact_cli/agents/base.py +95 -0
  5. specfact_cli/agents/plan_agent.py +202 -0
  6. specfact_cli/agents/registry.py +176 -0
  7. specfact_cli/agents/sync_agent.py +133 -0
  8. specfact_cli/analyzers/__init__.py +11 -0
  9. specfact_cli/analyzers/code_analyzer.py +796 -0
  10. specfact_cli/cli.py +396 -0
  11. specfact_cli/commands/__init__.py +7 -0
  12. specfact_cli/commands/enforce.py +88 -0
  13. specfact_cli/commands/import_cmd.py +365 -0
  14. specfact_cli/commands/init.py +125 -0
  15. specfact_cli/commands/plan.py +1089 -0
  16. specfact_cli/commands/repro.py +192 -0
  17. specfact_cli/commands/sync.py +408 -0
  18. specfact_cli/common/__init__.py +25 -0
  19. specfact_cli/common/logger_setup.py +654 -0
  20. specfact_cli/common/logging_utils.py +41 -0
  21. specfact_cli/common/text_utils.py +52 -0
  22. specfact_cli/common/utils.py +48 -0
  23. specfact_cli/comparators/__init__.py +11 -0
  24. specfact_cli/comparators/plan_comparator.py +391 -0
  25. specfact_cli/generators/__init__.py +14 -0
  26. specfact_cli/generators/plan_generator.py +105 -0
  27. specfact_cli/generators/protocol_generator.py +115 -0
  28. specfact_cli/generators/report_generator.py +200 -0
  29. specfact_cli/generators/workflow_generator.py +120 -0
  30. specfact_cli/importers/__init__.py +7 -0
  31. specfact_cli/importers/speckit_converter.py +773 -0
  32. specfact_cli/importers/speckit_scanner.py +711 -0
  33. specfact_cli/models/__init__.py +33 -0
  34. specfact_cli/models/deviation.py +105 -0
  35. specfact_cli/models/enforcement.py +150 -0
  36. specfact_cli/models/plan.py +97 -0
  37. specfact_cli/models/protocol.py +28 -0
  38. specfact_cli/modes/__init__.py +19 -0
  39. specfact_cli/modes/detector.py +126 -0
  40. specfact_cli/modes/router.py +153 -0
  41. specfact_cli/resources/semgrep/async.yml +285 -0
  42. specfact_cli/sync/__init__.py +12 -0
  43. specfact_cli/sync/repository_sync.py +279 -0
  44. specfact_cli/sync/speckit_sync.py +388 -0
  45. specfact_cli/utils/__init__.py +58 -0
  46. specfact_cli/utils/console.py +70 -0
  47. specfact_cli/utils/feature_keys.py +212 -0
  48. specfact_cli/utils/git.py +241 -0
  49. specfact_cli/utils/github_annotations.py +399 -0
  50. specfact_cli/utils/ide_setup.py +382 -0
  51. specfact_cli/utils/prompts.py +180 -0
  52. specfact_cli/utils/structure.py +497 -0
  53. specfact_cli/utils/yaml_utils.py +200 -0
  54. specfact_cli/validators/__init__.py +20 -0
  55. specfact_cli/validators/fsm.py +262 -0
  56. specfact_cli/validators/repro_checker.py +759 -0
  57. specfact_cli/validators/schema.py +196 -0
  58. specfact_cli-0.4.2.dist-info/METADATA +370 -0
  59. specfact_cli-0.4.2.dist-info/RECORD +62 -0
  60. specfact_cli-0.4.2.dist-info/WHEEL +4 -0
  61. specfact_cli-0.4.2.dist-info/entry_points.txt +2 -0
  62. specfact_cli-0.4.2.dist-info/licenses/LICENSE.md +61 -0
@@ -0,0 +1,153 @@
1
+ """
2
+ Command Router - Route commands based on operational mode.
3
+
4
+ This module provides routing logic to execute commands differently based on
5
+ the operational mode (CI/CD vs CoPilot).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Any
12
+
13
+ from beartype import beartype
14
+ from icontract import ensure, require
15
+
16
+ from specfact_cli.agents.registry import get_agent
17
+ from specfact_cli.modes.detector import OperationalMode, detect_mode
18
+
19
+
20
+ @dataclass
21
+ class RoutingResult:
22
+ """Result of command routing."""
23
+
24
+ execution_mode: str # "direct" or "agent"
25
+ mode: OperationalMode
26
+ command: str
27
+
28
+
29
+ class CommandRouter:
30
+ """Routes commands based on operational mode."""
31
+
32
+ @beartype
33
+ @require(lambda command: bool(command), "Command must be non-empty")
34
+ @require(lambda mode: isinstance(mode, OperationalMode), "Mode must be OperationalMode")
35
+ @ensure(lambda result: result.execution_mode in ("direct", "agent"), "Execution mode must be direct or agent")
36
+ @ensure(lambda result: result.mode in (OperationalMode.CICD, OperationalMode.COPILOT), "Mode must be valid")
37
+ def route(self, command: str, mode: OperationalMode, context: dict[str, Any] | None = None) -> RoutingResult:
38
+ """
39
+ Route a command based on operational mode.
40
+
41
+ Args:
42
+ command: Command name (e.g., "import from-code")
43
+ mode: Operational mode (CI/CD or CoPilot)
44
+ context: Optional context dictionary for command execution
45
+
46
+ Returns:
47
+ RoutingResult with execution mode and mode information
48
+
49
+ Examples:
50
+ >>> router = CommandRouter()
51
+ >>> result = router.route("import from-code", OperationalMode.CICD)
52
+ >>> result.execution_mode
53
+ 'direct'
54
+ >>> result = router.route("import from-code", OperationalMode.COPILOT)
55
+ >>> result.execution_mode
56
+ 'agent'
57
+ """
58
+ if mode == OperationalMode.CICD:
59
+ return RoutingResult(execution_mode="direct", mode=mode, command=command)
60
+ # CoPilot mode uses agent routing (Phase 4.1)
61
+ # Check if agent is available for this command
62
+ agent = get_agent(command)
63
+ if agent:
64
+ return RoutingResult(execution_mode="agent", mode=mode, command=command)
65
+ # Fallback to direct execution if no agent available
66
+ return RoutingResult(execution_mode="direct", mode=mode, command=command)
67
+
68
+ @beartype
69
+ @require(lambda command: bool(command), "Command must be non-empty")
70
+ @ensure(lambda result: result.mode in (OperationalMode.CICD, OperationalMode.COPILOT), "Mode must be valid")
71
+ def route_with_auto_detect(
72
+ self, command: str, explicit_mode: OperationalMode | None = None, context: dict[str, Any] | None = None
73
+ ) -> RoutingResult:
74
+ """
75
+ Route a command with automatic mode detection.
76
+
77
+ Args:
78
+ command: Command name (e.g., "import from-code")
79
+ explicit_mode: Optional explicit mode override
80
+ context: Optional context dictionary for command execution
81
+
82
+ Returns:
83
+ RoutingResult with execution mode and detected mode information
84
+
85
+ Examples:
86
+ >>> router = CommandRouter()
87
+ >>> result = router.route_with_auto_detect("import from-code")
88
+ >>> result.execution_mode in ("direct", "agent")
89
+ True
90
+ """
91
+ mode = detect_mode(explicit_mode=explicit_mode)
92
+ return self.route(command, mode, context)
93
+
94
+ @beartype
95
+ @require(lambda mode: isinstance(mode, OperationalMode), "Mode must be OperationalMode")
96
+ def should_use_agent(self, mode: OperationalMode) -> bool:
97
+ """
98
+ Check if command should use agent routing.
99
+
100
+ Args:
101
+ mode: Operational mode
102
+
103
+ Returns:
104
+ True if agent routing should be used, False for direct execution
105
+
106
+ Examples:
107
+ >>> router = CommandRouter()
108
+ >>> router.should_use_agent(OperationalMode.CICD)
109
+ False
110
+ >>> router.should_use_agent(OperationalMode.COPILOT)
111
+ True
112
+ """
113
+ return mode == OperationalMode.COPILOT
114
+
115
+ @beartype
116
+ @require(lambda mode: isinstance(mode, OperationalMode), "Mode must be OperationalMode")
117
+ def should_use_direct(self, mode: OperationalMode) -> bool:
118
+ """
119
+ Check if command should use direct execution.
120
+
121
+ Args:
122
+ mode: Operational mode
123
+
124
+ Returns:
125
+ True if direct execution should be used, False for agent routing
126
+
127
+ Examples:
128
+ >>> router = CommandRouter()
129
+ >>> router.should_use_direct(OperationalMode.CICD)
130
+ True
131
+ >>> router.should_use_direct(OperationalMode.COPILOT)
132
+ False
133
+ """
134
+ return mode == OperationalMode.CICD
135
+
136
+
137
+ def get_router() -> CommandRouter:
138
+ """
139
+ Get the global command router instance.
140
+
141
+ Returns:
142
+ CommandRouter instance
143
+
144
+ Examples:
145
+ >>> router = get_router()
146
+ >>> isinstance(router, CommandRouter)
147
+ True
148
+ """
149
+ return _router
150
+
151
+
152
+ # Global router instance
153
+ _router = CommandRouter()
@@ -0,0 +1,285 @@
1
+ rules:
2
+ - id: asyncio-create-task-not-awaited
3
+ patterns:
4
+ - pattern: asyncio.create_task(...)
5
+ - pattern-not-inside: await asyncio.create_task(...)
6
+ - pattern-not-inside: $TASK = asyncio.create_task(...)
7
+ message: |
8
+ Fire-and-forget task created without storing reference or awaiting.
9
+ This can lead to tasks being garbage collected before completion.
10
+ Either await the task or store the reference.
11
+ languages: [python]
12
+ severity: ERROR
13
+ metadata:
14
+ category: correctness
15
+ subcategory: [async]
16
+ likelihood: HIGH
17
+ impact: HIGH
18
+ confidence: HIGH
19
+
20
+ - id: blocking-sleep-in-async
21
+ patterns:
22
+ - pattern-either:
23
+ - pattern: time.sleep(...)
24
+ - pattern: time.wait(...)
25
+ - pattern-inside: |
26
+ async def $FUNC(...):
27
+ ...
28
+ message: |
29
+ Blocking sleep in async function. Use asyncio.sleep() instead.
30
+ Blocking calls prevent other coroutines from running.
31
+ languages: [python]
32
+ severity: ERROR
33
+ metadata:
34
+ category: correctness
35
+ subcategory: [async]
36
+ likelihood: HIGH
37
+ impact: HIGH
38
+ confidence: HIGH
39
+ fix: asyncio.sleep(...)
40
+
41
+ - id: missing-await-on-coroutine
42
+ patterns:
43
+ - pattern: $FUNC(...)
44
+ - pattern-not: await $FUNC(...)
45
+ - pattern-not: asyncio.create_task($FUNC(...))
46
+ - pattern-not: asyncio.gather($FUNC(...), ...)
47
+ - pattern-inside: |
48
+ async def $OUTER(...):
49
+ ...
50
+ message: |
51
+ Coroutine call without await. This creates a coroutine object but never executes it.
52
+ Add 'await' keyword or use asyncio.create_task() for background execution.
53
+ languages: [python]
54
+ severity: ERROR
55
+ metadata:
56
+ category: correctness
57
+ subcategory: [async]
58
+ likelihood: HIGH
59
+ impact: HIGH
60
+ confidence: MEDIUM
61
+
62
+ - id: bare-except-in-async
63
+ patterns:
64
+ - pattern-either:
65
+ - pattern: |
66
+ try:
67
+ ...
68
+ except:
69
+ ...
70
+ - pattern: |
71
+ try:
72
+ ...
73
+ except Exception:
74
+ pass
75
+ - pattern-inside: |
76
+ async def $FUNC(...):
77
+ ...
78
+ message: |
79
+ Bare except or silent exception handling in async function.
80
+ This can hide errors in coroutines and make debugging difficult.
81
+ Use specific exception types and log errors.
82
+ languages: [python]
83
+ severity: WARNING
84
+ metadata:
85
+ category: correctness
86
+ subcategory: [async, error-handling]
87
+ likelihood: MEDIUM
88
+ impact: MEDIUM
89
+ confidence: HIGH
90
+
91
+ - id: missing-timeout-on-wait
92
+ patterns:
93
+ - pattern-either:
94
+ - pattern: await asyncio.wait_for($CORO, None)
95
+ - pattern: await $CORO
96
+ - pattern-not: await asyncio.wait_for($CORO, timeout=...)
97
+ - pattern-inside: |
98
+ async def $FUNC(...):
99
+ ...
100
+ message: |
101
+ Async wait without timeout. Long-running operations should have timeouts
102
+ to prevent indefinite hangs. Use asyncio.wait_for(coro, timeout=...).
103
+ languages: [python]
104
+ severity: WARNING
105
+ metadata:
106
+ category: correctness
107
+ subcategory: [async, timeout]
108
+ likelihood: MEDIUM
109
+ impact: MEDIUM
110
+ confidence: LOW
111
+
112
+ - id: blocking-file-io-in-async
113
+ patterns:
114
+ - pattern-either:
115
+ - pattern: open(...)
116
+ - pattern: $FILE.read(...)
117
+ - pattern: $FILE.write(...)
118
+ - pattern-not-inside: |
119
+ with aiofiles.open(...) as $F:
120
+ ...
121
+ - pattern-inside: |
122
+ async def $FUNC(...):
123
+ ...
124
+ message: |
125
+ Blocking file I/O in async function. Use aiofiles or run_in_executor()
126
+ for file operations to avoid blocking the event loop.
127
+ languages: [python]
128
+ severity: WARNING
129
+ metadata:
130
+ category: performance
131
+ subcategory: [async, io]
132
+ likelihood: MEDIUM
133
+ impact: MEDIUM
134
+ confidence: MEDIUM
135
+
136
+ - id: asyncio-gather-without-error-handling
137
+ patterns:
138
+ - pattern: await asyncio.gather(...)
139
+ - pattern-not-inside: |
140
+ try:
141
+ await asyncio.gather(...)
142
+ except ...:
143
+ ...
144
+ - pattern-not: await asyncio.gather(..., return_exceptions=True)
145
+ message: |
146
+ asyncio.gather() without error handling. If any coroutine raises an exception,
147
+ gather() will raise it immediately. Use return_exceptions=True or wrap in try/except.
148
+ languages: [python]
149
+ severity: WARNING
150
+ metadata:
151
+ category: correctness
152
+ subcategory: [async, error-handling]
153
+ likelihood: MEDIUM
154
+ impact: MEDIUM
155
+ confidence: HIGH
156
+
157
+ - id: event-loop-in-async-context
158
+ patterns:
159
+ - pattern-either:
160
+ - pattern: asyncio.get_event_loop().run_until_complete(...)
161
+ - pattern: asyncio.run(...)
162
+ - pattern-inside: |
163
+ async def $FUNC(...):
164
+ ...
165
+ message: |
166
+ Running event loop inside async context. This creates a nested event loop
167
+ which can cause deadlocks. Use 'await' instead of run_until_complete().
168
+ languages: [python]
169
+ severity: ERROR
170
+ metadata:
171
+ category: correctness
172
+ subcategory: [async]
173
+ likelihood: HIGH
174
+ impact: HIGH
175
+ confidence: HIGH
176
+
177
+ - id: missing-async-context-manager
178
+ patterns:
179
+ - pattern: |
180
+ async with $RESOURCE:
181
+ ...
182
+ - pattern-not: |
183
+ async with $RESOURCE as $VAR:
184
+ ...
185
+ message: |
186
+ Async context manager without variable binding. Consider binding the resource
187
+ to a variable for explicit resource management.
188
+ languages: [python]
189
+ severity: INFO
190
+ metadata:
191
+ category: best-practice
192
+ subcategory: [async]
193
+ likelihood: LOW
194
+ impact: LOW
195
+ confidence: MEDIUM
196
+
197
+ - id: sync-lock-in-async
198
+ patterns:
199
+ - pattern-either:
200
+ - pattern: threading.Lock()
201
+ - pattern: threading.RLock()
202
+ - pattern: threading.Semaphore()
203
+ - pattern-inside: |
204
+ async def $FUNC(...):
205
+ ...
206
+ message: |
207
+ Using synchronous lock in async function. Use asyncio.Lock() or
208
+ asyncio.Semaphore() instead to avoid blocking the event loop.
209
+ languages: [python]
210
+ severity: ERROR
211
+ metadata:
212
+ category: correctness
213
+ subcategory: [async, concurrency]
214
+ likelihood: HIGH
215
+ impact: HIGH
216
+ confidence: HIGH
217
+
218
+ - id: sequential-await-could-be-parallel
219
+ patterns:
220
+ - pattern: |
221
+ await $FUNC1(...)
222
+ await $FUNC2(...)
223
+ - pattern-not-inside: |
224
+ results = await asyncio.gather(
225
+ $FUNC1(...),
226
+ $FUNC2(...),
227
+ )
228
+ message: |
229
+ Sequential awaits that could be parallelized. If these operations are
230
+ independent, use asyncio.gather() to run them concurrently.
231
+ languages: [python]
232
+ severity: INFO
233
+ metadata:
234
+ category: performance
235
+ subcategory: [async]
236
+ likelihood: LOW
237
+ impact: LOW
238
+ confidence: LOW
239
+
240
+ - id: missing-cancellation-handling
241
+ patterns:
242
+ - pattern: |
243
+ async def $FUNC(...):
244
+ ...
245
+ - pattern-not-inside: |
246
+ try:
247
+ ...
248
+ except asyncio.CancelledError:
249
+ ...
250
+ message: |
251
+ Async function without cancellation handling. Long-running tasks should
252
+ handle CancelledError to clean up resources properly.
253
+ languages: [python]
254
+ severity: INFO
255
+ metadata:
256
+ category: best-practice
257
+ subcategory: [async, cleanup]
258
+ likelihood: LOW
259
+ impact: MEDIUM
260
+ confidence: LOW
261
+
262
+ - id: task-result-not-checked
263
+ patterns:
264
+ - pattern: |
265
+ $TASK = asyncio.create_task(...)
266
+ ...
267
+ - pattern-not: |
268
+ $TASK = asyncio.create_task(...)
269
+ ...
270
+ $RESULT = await $TASK
271
+ - pattern-not: |
272
+ $TASK = asyncio.create_task(...)
273
+ ...
274
+ $TASK.result()
275
+ message: |
276
+ Task created but result never checked. Background tasks may fail silently.
277
+ Await the task or check its result/exception.
278
+ languages: [python]
279
+ severity: WARNING
280
+ metadata:
281
+ category: correctness
282
+ subcategory: [async]
283
+ likelihood: MEDIUM
284
+ impact: MEDIUM
285
+ confidence: LOW
@@ -0,0 +1,12 @@
1
+ """
2
+ Sync operations for SpecFact CLI.
3
+
4
+ This module provides bidirectional synchronization between Spec-Kit artifacts,
5
+ repository changes, and SpecFact plans.
6
+ """
7
+
8
+ from specfact_cli.sync.repository_sync import RepositorySync, RepositorySyncResult
9
+ from specfact_cli.sync.speckit_sync import SpecKitSync, SyncResult
10
+
11
+
12
+ __all__ = ["RepositorySync", "RepositorySyncResult", "SpecKitSync", "SyncResult"]