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.
- specfact_cli/__init__.py +14 -0
- specfact_cli/agents/__init__.py +24 -0
- specfact_cli/agents/analyze_agent.py +392 -0
- specfact_cli/agents/base.py +95 -0
- specfact_cli/agents/plan_agent.py +202 -0
- specfact_cli/agents/registry.py +176 -0
- specfact_cli/agents/sync_agent.py +133 -0
- specfact_cli/analyzers/__init__.py +11 -0
- specfact_cli/analyzers/code_analyzer.py +796 -0
- specfact_cli/cli.py +396 -0
- specfact_cli/commands/__init__.py +7 -0
- specfact_cli/commands/enforce.py +88 -0
- specfact_cli/commands/import_cmd.py +365 -0
- specfact_cli/commands/init.py +125 -0
- specfact_cli/commands/plan.py +1089 -0
- specfact_cli/commands/repro.py +192 -0
- specfact_cli/commands/sync.py +408 -0
- specfact_cli/common/__init__.py +25 -0
- specfact_cli/common/logger_setup.py +654 -0
- specfact_cli/common/logging_utils.py +41 -0
- specfact_cli/common/text_utils.py +52 -0
- specfact_cli/common/utils.py +48 -0
- specfact_cli/comparators/__init__.py +11 -0
- specfact_cli/comparators/plan_comparator.py +391 -0
- specfact_cli/generators/__init__.py +14 -0
- specfact_cli/generators/plan_generator.py +105 -0
- specfact_cli/generators/protocol_generator.py +115 -0
- specfact_cli/generators/report_generator.py +200 -0
- specfact_cli/generators/workflow_generator.py +120 -0
- specfact_cli/importers/__init__.py +7 -0
- specfact_cli/importers/speckit_converter.py +773 -0
- specfact_cli/importers/speckit_scanner.py +711 -0
- specfact_cli/models/__init__.py +33 -0
- specfact_cli/models/deviation.py +105 -0
- specfact_cli/models/enforcement.py +150 -0
- specfact_cli/models/plan.py +97 -0
- specfact_cli/models/protocol.py +28 -0
- specfact_cli/modes/__init__.py +19 -0
- specfact_cli/modes/detector.py +126 -0
- specfact_cli/modes/router.py +153 -0
- specfact_cli/resources/semgrep/async.yml +285 -0
- specfact_cli/sync/__init__.py +12 -0
- specfact_cli/sync/repository_sync.py +279 -0
- specfact_cli/sync/speckit_sync.py +388 -0
- specfact_cli/utils/__init__.py +58 -0
- specfact_cli/utils/console.py +70 -0
- specfact_cli/utils/feature_keys.py +212 -0
- specfact_cli/utils/git.py +241 -0
- specfact_cli/utils/github_annotations.py +399 -0
- specfact_cli/utils/ide_setup.py +382 -0
- specfact_cli/utils/prompts.py +180 -0
- specfact_cli/utils/structure.py +497 -0
- specfact_cli/utils/yaml_utils.py +200 -0
- specfact_cli/validators/__init__.py +20 -0
- specfact_cli/validators/fsm.py +262 -0
- specfact_cli/validators/repro_checker.py +759 -0
- specfact_cli/validators/schema.py +196 -0
- specfact_cli-0.4.2.dist-info/METADATA +370 -0
- specfact_cli-0.4.2.dist-info/RECORD +62 -0
- specfact_cli-0.4.2.dist-info/WHEEL +4 -0
- specfact_cli-0.4.2.dist-info/entry_points.txt +2 -0
- 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"]
|