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,20 @@
1
+ """
2
+ SpecFact CLI validators.
3
+
4
+ This package contains validation logic for schemas, contracts,
5
+ protocols, and plans.
6
+ """
7
+
8
+ from specfact_cli.validators.fsm import FSMValidator
9
+ from specfact_cli.validators.repro_checker import ReproChecker, ReproReport
10
+ from specfact_cli.validators.schema import SchemaValidator, validate_plan_bundle, validate_protocol
11
+
12
+
13
+ __all__ = [
14
+ "FSMValidator",
15
+ "ReproChecker",
16
+ "ReproReport",
17
+ "SchemaValidator",
18
+ "validate_plan_bundle",
19
+ "validate_protocol",
20
+ ]
@@ -0,0 +1,262 @@
1
+ """
2
+ FSM (Finite State Machine) validation module.
3
+
4
+ This module provides validators for state machine protocols and transitions.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ import networkx as nx
12
+ from beartype import beartype
13
+ from icontract import ensure, require
14
+
15
+ from specfact_cli.models.deviation import Deviation, DeviationSeverity, DeviationType, ValidationReport
16
+ from specfact_cli.models.protocol import Protocol
17
+ from specfact_cli.utils.yaml_utils import load_yaml
18
+
19
+
20
+ class FSMValidator:
21
+ """FSM validator for protocol validation."""
22
+
23
+ @beartype
24
+ @require(
25
+ lambda protocol, protocol_path: protocol is not None or protocol_path is not None,
26
+ "Either protocol or protocol_path must be provided",
27
+ )
28
+ @require(
29
+ lambda protocol_path: protocol_path is None or protocol_path.exists(), "Protocol path must exist if provided"
30
+ )
31
+ def __init__(
32
+ self,
33
+ protocol: Protocol | None = None,
34
+ protocol_path: Path | None = None,
35
+ guard_functions: dict | None = None,
36
+ ) -> None:
37
+ """
38
+ Initialize FSM validator.
39
+
40
+ Args:
41
+ protocol: Protocol model to validate
42
+ protocol_path: Path to protocol YAML file (must exist if provided)
43
+ guard_functions: Optional dict of guard function implementations
44
+
45
+ Raises:
46
+ ValueError: If neither protocol nor protocol_path is provided
47
+ """
48
+
49
+ if protocol is None:
50
+ # Load protocol from file
51
+ data = load_yaml(protocol_path) # type: ignore
52
+ self.protocol = Protocol(**data)
53
+ else:
54
+ self.protocol = protocol
55
+
56
+ self.guard_functions = guard_functions if guard_functions is not None else {}
57
+ self.graph = self._build_graph()
58
+
59
+ def _build_graph(self) -> nx.DiGraph:
60
+ """
61
+ Build directed graph from protocol transitions.
62
+
63
+ Returns:
64
+ NetworkX directed graph
65
+ """
66
+ graph = nx.DiGraph()
67
+
68
+ # Add all states as nodes
69
+ for state in self.protocol.states:
70
+ graph.add_node(state)
71
+
72
+ # Add transitions as edges
73
+ for transition in self.protocol.transitions:
74
+ graph.add_edge(
75
+ transition.from_state,
76
+ transition.to_state,
77
+ event=transition.on_event,
78
+ guard=transition.guard,
79
+ )
80
+
81
+ return graph
82
+
83
+ @beartype
84
+ @ensure(lambda result: isinstance(result, ValidationReport), "Must return ValidationReport")
85
+ def validate(self) -> ValidationReport:
86
+ """
87
+ Validate the FSM protocol.
88
+
89
+ Returns:
90
+ Validation report with any deviations found
91
+ """
92
+ report = ValidationReport()
93
+
94
+ # Check 1: Start state exists
95
+ if self.protocol.start not in self.protocol.states:
96
+ report.add_deviation(
97
+ Deviation(
98
+ type=DeviationType.FSM_MISMATCH,
99
+ severity=DeviationSeverity.HIGH,
100
+ description=f"Start state '{self.protocol.start}' not in states list",
101
+ location="protocol.start",
102
+ fix_hint=f"Add '{self.protocol.start}' to states list",
103
+ )
104
+ )
105
+
106
+ # Check 2: All transition states exist
107
+ for transition in self.protocol.transitions:
108
+ if transition.from_state not in self.protocol.states:
109
+ report.add_deviation(
110
+ Deviation(
111
+ type=DeviationType.FSM_MISMATCH,
112
+ severity=DeviationSeverity.HIGH,
113
+ description=f"Transition from unknown state: '{transition.from_state}'",
114
+ location=f"transition[{transition.from_state} → {transition.to_state}]",
115
+ fix_hint=f"Add '{transition.from_state}' to states list",
116
+ )
117
+ )
118
+
119
+ if transition.to_state not in self.protocol.states:
120
+ report.add_deviation(
121
+ Deviation(
122
+ type=DeviationType.FSM_MISMATCH,
123
+ severity=DeviationSeverity.HIGH,
124
+ description=f"Transition to unknown state: '{transition.to_state}'",
125
+ location=f"transition[{transition.from_state} → {transition.to_state}]",
126
+ fix_hint=f"Add '{transition.to_state}' to states list",
127
+ )
128
+ )
129
+
130
+ # Check 3: Reachability - all states reachable from start
131
+ if report.passed: # Only if no critical errors so far
132
+ reachable = nx.descendants(self.graph, self.protocol.start)
133
+ reachable.add(self.protocol.start)
134
+
135
+ unreachable = set(self.protocol.states) - reachable
136
+ if unreachable:
137
+ for state in unreachable:
138
+ report.add_deviation(
139
+ Deviation(
140
+ type=DeviationType.FSM_MISMATCH,
141
+ severity=DeviationSeverity.MEDIUM,
142
+ description=f"State '{state}' is not reachable from start state",
143
+ location=f"state[{state}]",
144
+ fix_hint=f"Add transition path from '{self.protocol.start}' to '{state}'",
145
+ )
146
+ )
147
+
148
+ # Check 4: Guards are defined
149
+ for transition in self.protocol.transitions:
150
+ if (
151
+ transition.guard
152
+ and transition.guard not in self.protocol.guards
153
+ and transition.guard not in self.guard_functions
154
+ ):
155
+ # LOW severity if guard functions can be provided externally
156
+ report.add_deviation(
157
+ Deviation(
158
+ type=DeviationType.FSM_MISMATCH,
159
+ severity=DeviationSeverity.LOW,
160
+ description=f"Guard '{transition.guard}' not defined in protocol or guard_functions",
161
+ location=f"transition[{transition.from_state} → {transition.to_state}]",
162
+ fix_hint=f"Add guard definition for '{transition.guard}' in protocol.guards or pass guard_functions",
163
+ )
164
+ )
165
+
166
+ # Check 5: Detect cycles (informational)
167
+ try:
168
+ cycles = list(nx.simple_cycles(self.graph))
169
+ if cycles:
170
+ for cycle in cycles:
171
+ report.add_deviation(
172
+ Deviation(
173
+ type=DeviationType.FSM_MISMATCH,
174
+ severity=DeviationSeverity.LOW,
175
+ description=f"Cycle detected: {' → '.join(cycle)}",
176
+ location="protocol.transitions",
177
+ fix_hint="Cycles may be intentional for workflows, verify this is expected",
178
+ )
179
+ )
180
+ except nx.NetworkXNoCycle:
181
+ pass # No cycles is fine
182
+
183
+ return report
184
+
185
+ @beartype
186
+ @require(lambda from_state: isinstance(from_state, str) and len(from_state) > 0, "State must be non-empty string")
187
+ @ensure(lambda result: isinstance(result, set), "Must return set")
188
+ @ensure(lambda result: all(isinstance(s, str) for s in result), "All items must be strings")
189
+ def get_reachable_states(self, from_state: str) -> set[str]:
190
+ """
191
+ Get all states reachable from given state.
192
+
193
+ Args:
194
+ from_state: Starting state
195
+
196
+ Returns:
197
+ Set of reachable state names
198
+ """
199
+ if from_state not in self.protocol.states:
200
+ return set()
201
+
202
+ reachable = nx.descendants(self.graph, from_state)
203
+ reachable.add(from_state)
204
+ return reachable
205
+
206
+ @beartype
207
+ @require(lambda state: isinstance(state, str) and len(state) > 0, "State must be non-empty string")
208
+ @ensure(lambda result: isinstance(result, list), "Must return list")
209
+ @ensure(lambda result: all(isinstance(t, dict) for t in result), "All items must be dictionaries")
210
+ def get_transitions_from(self, state: str) -> list[dict]:
211
+ """
212
+ Get all transitions from given state.
213
+
214
+ Args:
215
+ state: State name
216
+
217
+ Returns:
218
+ List of transition dictionaries
219
+ """
220
+ if state not in self.protocol.states:
221
+ return []
222
+
223
+ transitions = []
224
+ for successor in self.graph.successors(state):
225
+ edge_data = self.graph.get_edge_data(state, successor)
226
+ transitions.append(
227
+ {
228
+ "from_state": state,
229
+ "to_state": successor,
230
+ "event": edge_data.get("event"),
231
+ "guard": edge_data.get("guard"),
232
+ }
233
+ )
234
+
235
+ return transitions
236
+
237
+ @beartype
238
+ @require(
239
+ lambda from_state: isinstance(from_state, str) and len(from_state) > 0, "From state must be non-empty string"
240
+ )
241
+ @require(lambda on_event: isinstance(on_event, str) and len(on_event) > 0, "Event must be non-empty string")
242
+ @require(lambda to_state: isinstance(to_state, str) and len(to_state) > 0, "To state must be non-empty string")
243
+ @ensure(lambda result: isinstance(result, bool), "Must return boolean")
244
+ def is_valid_transition(self, from_state: str, on_event: str, to_state: str) -> bool:
245
+ """
246
+ Check if transition is valid.
247
+
248
+ Args:
249
+ from_state: Source state
250
+ on_event: Event that triggers the transition
251
+ to_state: Target state
252
+
253
+ Returns:
254
+ True if transition exists with the given event
255
+ """
256
+ # Check if edge exists
257
+ if not self.graph.has_edge(from_state, to_state):
258
+ return False
259
+
260
+ # Check if the event matches
261
+ edge_data = self.graph.get_edge_data(from_state, to_state)
262
+ return edge_data.get("event") == on_event