specfact-cli 0.4.0__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.
Potentially problematic release.
This version of specfact-cli might be problematic. Click here for more details.
- specfact_cli/__init__.py +14 -0
- specfact_cli/agents/__init__.py +23 -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 +10 -0
- specfact_cli/analyzers/code_analyzer.py +775 -0
- specfact_cli/cli.py +397 -0
- specfact_cli/commands/__init__.py +7 -0
- specfact_cli/commands/enforce.py +87 -0
- specfact_cli/commands/import_cmd.py +355 -0
- specfact_cli/commands/init.py +119 -0
- specfact_cli/commands/plan.py +1090 -0
- specfact_cli/commands/repro.py +172 -0
- specfact_cli/commands/sync.py +408 -0
- specfact_cli/common/__init__.py +24 -0
- specfact_cli/common/logger_setup.py +673 -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 +10 -0
- specfact_cli/comparators/plan_comparator.py +391 -0
- specfact_cli/generators/__init__.py +13 -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 +111 -0
- specfact_cli/importers/__init__.py +6 -0
- specfact_cli/importers/speckit_converter.py +773 -0
- specfact_cli/importers/speckit_scanner.py +704 -0
- specfact_cli/models/__init__.py +32 -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 +18 -0
- specfact_cli/modes/detector.py +126 -0
- specfact_cli/modes/router.py +153 -0
- specfact_cli/sync/__init__.py +11 -0
- specfact_cli/sync/repository_sync.py +279 -0
- specfact_cli/sync/speckit_sync.py +388 -0
- specfact_cli/utils/__init__.py +57 -0
- specfact_cli/utils/console.py +69 -0
- specfact_cli/utils/feature_keys.py +213 -0
- specfact_cli/utils/git.py +241 -0
- specfact_cli/utils/ide_setup.py +381 -0
- specfact_cli/utils/prompts.py +179 -0
- specfact_cli/utils/structure.py +496 -0
- specfact_cli/utils/yaml_utils.py +200 -0
- specfact_cli/validators/__init__.py +19 -0
- specfact_cli/validators/fsm.py +260 -0
- specfact_cli/validators/repro_checker.py +320 -0
- specfact_cli/validators/schema.py +200 -0
- specfact_cli-0.4.0.dist-info/METADATA +332 -0
- specfact_cli-0.4.0.dist-info/RECORD +60 -0
- specfact_cli-0.4.0.dist-info/WHEEL +4 -0
- specfact_cli-0.4.0.dist-info/entry_points.txt +2 -0
- specfact_cli-0.4.0.dist-info/licenses/LICENSE.md +55 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SpecFact CLI data models.
|
|
3
|
+
|
|
4
|
+
This package contains Pydantic models for plan bundles, protocols,
|
|
5
|
+
features, stories, and validation results.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from specfact_cli.models.deviation import Deviation, DeviationReport, DeviationSeverity, DeviationType, ValidationReport
|
|
9
|
+
from specfact_cli.models.enforcement import EnforcementAction, EnforcementConfig, EnforcementPreset
|
|
10
|
+
from specfact_cli.models.plan import Business, Feature, Idea, Metadata, PlanBundle, Product, Release, Story
|
|
11
|
+
from specfact_cli.models.protocol import Protocol, Transition
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Business",
|
|
15
|
+
"Deviation",
|
|
16
|
+
"DeviationReport",
|
|
17
|
+
"DeviationSeverity",
|
|
18
|
+
"DeviationType",
|
|
19
|
+
"EnforcementAction",
|
|
20
|
+
"EnforcementConfig",
|
|
21
|
+
"EnforcementPreset",
|
|
22
|
+
"Feature",
|
|
23
|
+
"Idea",
|
|
24
|
+
"Metadata",
|
|
25
|
+
"PlanBundle",
|
|
26
|
+
"Product",
|
|
27
|
+
"Protocol",
|
|
28
|
+
"Release",
|
|
29
|
+
"Story",
|
|
30
|
+
"Transition",
|
|
31
|
+
"ValidationReport",
|
|
32
|
+
]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Deviation tracking models.
|
|
3
|
+
|
|
4
|
+
This module defines models for tracking deviations between plans,
|
|
5
|
+
protocols, and actual implementation following the CLI-First specification.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
from beartype import beartype
|
|
13
|
+
from icontract import ensure, require
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DeviationSeverity(str, Enum):
|
|
18
|
+
"""Deviation severity level."""
|
|
19
|
+
|
|
20
|
+
HIGH = "HIGH"
|
|
21
|
+
MEDIUM = "MEDIUM"
|
|
22
|
+
LOW = "LOW"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DeviationType(str, Enum):
|
|
26
|
+
"""Type of deviation."""
|
|
27
|
+
|
|
28
|
+
MISSING_FEATURE = "missing_feature"
|
|
29
|
+
MISSING_STORY = "missing_story"
|
|
30
|
+
MISSING_BUSINESS_CONTEXT = "missing_business_context"
|
|
31
|
+
EXTRA_FEATURE = "extra_feature"
|
|
32
|
+
EXTRA_STORY = "extra_story"
|
|
33
|
+
EXTRA_IMPLEMENTATION = "extra_implementation"
|
|
34
|
+
MISMATCH = "mismatch"
|
|
35
|
+
ACCEPTANCE_DRIFT = "acceptance_drift"
|
|
36
|
+
FSM_MISMATCH = "fsm_mismatch"
|
|
37
|
+
RISK_OMISSION = "risk_omission"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Deviation(BaseModel):
|
|
41
|
+
"""Deviation model."""
|
|
42
|
+
|
|
43
|
+
type: DeviationType = Field(..., description="Deviation type")
|
|
44
|
+
severity: DeviationSeverity = Field(..., description="Severity level")
|
|
45
|
+
description: str = Field(..., description="Deviation description")
|
|
46
|
+
location: str = Field(..., description="File/module location")
|
|
47
|
+
fix_hint: str | None = Field(None, description="Fix suggestion")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class DeviationReport(BaseModel):
|
|
51
|
+
"""Deviation report model."""
|
|
52
|
+
|
|
53
|
+
manual_plan: str = Field(..., description="Path to manual plan bundle")
|
|
54
|
+
auto_plan: str = Field(..., description="Path to auto-generated plan bundle")
|
|
55
|
+
deviations: list[Deviation] = Field(default_factory=list, description="All deviations")
|
|
56
|
+
summary: dict[str, int] = Field(default_factory=dict, description="Deviation counts by type")
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def total_deviations(self) -> int:
|
|
60
|
+
"""Total number of deviations."""
|
|
61
|
+
return len(self.deviations)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def high_count(self) -> int:
|
|
65
|
+
"""Number of high severity deviations."""
|
|
66
|
+
return sum(1 for d in self.deviations if d.severity == DeviationSeverity.HIGH)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def medium_count(self) -> int:
|
|
70
|
+
"""Number of medium severity deviations."""
|
|
71
|
+
return sum(1 for d in self.deviations if d.severity == DeviationSeverity.MEDIUM)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def low_count(self) -> int:
|
|
75
|
+
"""Number of low severity deviations."""
|
|
76
|
+
return sum(1 for d in self.deviations if d.severity == DeviationSeverity.LOW)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ValidationReport(BaseModel):
|
|
80
|
+
"""Validation report model (for backward compatibility)."""
|
|
81
|
+
|
|
82
|
+
deviations: list[Deviation] = Field(default_factory=list, description="All deviations")
|
|
83
|
+
high_count: int = Field(default=0, description="Number of high severity deviations")
|
|
84
|
+
medium_count: int = Field(default=0, description="Number of medium severity deviations")
|
|
85
|
+
low_count: int = Field(default=0, description="Number of low severity deviations")
|
|
86
|
+
passed: bool = Field(default=True, description="Whether validation passed")
|
|
87
|
+
|
|
88
|
+
@beartype
|
|
89
|
+
@require(lambda deviation: isinstance(deviation, Deviation), "Must be Deviation instance")
|
|
90
|
+
@ensure(
|
|
91
|
+
lambda self: self.high_count + self.medium_count + self.low_count == len(self.deviations),
|
|
92
|
+
"Counts must match deviations",
|
|
93
|
+
)
|
|
94
|
+
@ensure(lambda self: self.passed == (self.high_count == 0), "Must fail if high severity deviations exist")
|
|
95
|
+
def add_deviation(self, deviation: Deviation) -> None:
|
|
96
|
+
"""Add a deviation and update counts."""
|
|
97
|
+
self.deviations.append(deviation)
|
|
98
|
+
|
|
99
|
+
if deviation.severity == DeviationSeverity.HIGH:
|
|
100
|
+
self.high_count += 1
|
|
101
|
+
self.passed = False
|
|
102
|
+
elif deviation.severity == DeviationSeverity.MEDIUM:
|
|
103
|
+
self.medium_count += 1
|
|
104
|
+
elif deviation.severity == DeviationSeverity.LOW:
|
|
105
|
+
self.low_count += 1
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Enforcement configuration models for quality gates."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
from beartype import beartype
|
|
6
|
+
from icontract import ensure, require
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EnforcementAction(str, Enum):
|
|
11
|
+
"""Actions that can be taken when a deviation is detected."""
|
|
12
|
+
|
|
13
|
+
BLOCK = "BLOCK" # Fail the validation (exit code 1)
|
|
14
|
+
WARN = "WARN" # Show warning but continue (exit code 0)
|
|
15
|
+
LOG = "LOG" # Only log, no warning (exit code 0)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EnforcementPreset(str, Enum):
|
|
19
|
+
"""Predefined enforcement presets."""
|
|
20
|
+
|
|
21
|
+
MINIMAL = "minimal" # Log everything, never block
|
|
22
|
+
BALANCED = "balanced" # Block HIGH, warn MEDIUM, log LOW
|
|
23
|
+
STRICT = "strict" # Block HIGH+MEDIUM, warn LOW
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EnforcementConfig(BaseModel):
|
|
27
|
+
"""Configuration for contract enforcement and quality gates."""
|
|
28
|
+
|
|
29
|
+
preset: EnforcementPreset = Field(default=EnforcementPreset.BALANCED, description="Enforcement preset mode")
|
|
30
|
+
high_action: EnforcementAction = Field(
|
|
31
|
+
default=EnforcementAction.BLOCK, description="Action for HIGH severity deviations"
|
|
32
|
+
)
|
|
33
|
+
medium_action: EnforcementAction = Field(
|
|
34
|
+
default=EnforcementAction.WARN, description="Action for MEDIUM severity deviations"
|
|
35
|
+
)
|
|
36
|
+
low_action: EnforcementAction = Field(
|
|
37
|
+
default=EnforcementAction.LOG, description="Action for LOW severity deviations"
|
|
38
|
+
)
|
|
39
|
+
enabled: bool = Field(default=True, description="Whether enforcement is enabled")
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
@beartype
|
|
43
|
+
@require(lambda preset: preset in EnforcementPreset, "Preset must be valid EnforcementPreset")
|
|
44
|
+
@ensure(lambda result: isinstance(result, EnforcementConfig), "Must return EnforcementConfig")
|
|
45
|
+
@ensure(lambda result: result.enabled is True, "Config must be enabled")
|
|
46
|
+
def from_preset(cls, preset: EnforcementPreset) -> "EnforcementConfig":
|
|
47
|
+
"""
|
|
48
|
+
Create an enforcement config from a preset.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
preset: The preset to use
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
EnforcementConfig with preset values
|
|
55
|
+
"""
|
|
56
|
+
if preset == EnforcementPreset.MINIMAL:
|
|
57
|
+
return cls(
|
|
58
|
+
preset=preset,
|
|
59
|
+
high_action=EnforcementAction.WARN,
|
|
60
|
+
medium_action=EnforcementAction.WARN,
|
|
61
|
+
low_action=EnforcementAction.LOG,
|
|
62
|
+
enabled=True,
|
|
63
|
+
)
|
|
64
|
+
if preset == EnforcementPreset.BALANCED:
|
|
65
|
+
return cls(
|
|
66
|
+
preset=preset,
|
|
67
|
+
high_action=EnforcementAction.BLOCK,
|
|
68
|
+
medium_action=EnforcementAction.WARN,
|
|
69
|
+
low_action=EnforcementAction.LOG,
|
|
70
|
+
enabled=True,
|
|
71
|
+
)
|
|
72
|
+
if preset == EnforcementPreset.STRICT:
|
|
73
|
+
return cls(
|
|
74
|
+
preset=preset,
|
|
75
|
+
high_action=EnforcementAction.BLOCK,
|
|
76
|
+
medium_action=EnforcementAction.BLOCK,
|
|
77
|
+
low_action=EnforcementAction.WARN,
|
|
78
|
+
enabled=True,
|
|
79
|
+
)
|
|
80
|
+
# Default to balanced
|
|
81
|
+
return cls.from_preset(EnforcementPreset.BALANCED)
|
|
82
|
+
|
|
83
|
+
@beartype
|
|
84
|
+
@require(lambda severity: isinstance(severity, str) and len(severity) > 0, "Severity must be non-empty string")
|
|
85
|
+
@require(lambda severity: severity.upper() in ("HIGH", "MEDIUM", "LOW"), "Severity must be HIGH/MEDIUM/LOW")
|
|
86
|
+
@ensure(lambda result: isinstance(result, bool), "Must return boolean")
|
|
87
|
+
def should_block_deviation(self, severity: str) -> bool:
|
|
88
|
+
"""
|
|
89
|
+
Determine if a deviation should block execution.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
severity: Deviation severity (HIGH, MEDIUM, LOW)
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
True if this deviation should cause a failure (exit 1)
|
|
96
|
+
"""
|
|
97
|
+
if not self.enabled:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
severity_upper = severity.upper()
|
|
101
|
+
if severity_upper == "HIGH":
|
|
102
|
+
return self.high_action == EnforcementAction.BLOCK
|
|
103
|
+
if severity_upper == "MEDIUM":
|
|
104
|
+
return self.medium_action == EnforcementAction.BLOCK
|
|
105
|
+
if severity_upper == "LOW":
|
|
106
|
+
return self.low_action == EnforcementAction.BLOCK
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
@beartype
|
|
110
|
+
@require(lambda severity: isinstance(severity, str) and len(severity) > 0, "Severity must be non-empty string")
|
|
111
|
+
@require(lambda severity: severity.upper() in ("HIGH", "MEDIUM", "LOW"), "Severity must be HIGH/MEDIUM/LOW")
|
|
112
|
+
@ensure(lambda result: isinstance(result, EnforcementAction), "Must return EnforcementAction")
|
|
113
|
+
def get_action(self, severity: str) -> EnforcementAction:
|
|
114
|
+
"""
|
|
115
|
+
Get the action for a given severity level.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
severity: Deviation severity (HIGH, MEDIUM, LOW)
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
The enforcement action to take
|
|
122
|
+
"""
|
|
123
|
+
severity_upper = severity.upper()
|
|
124
|
+
if severity_upper == "HIGH":
|
|
125
|
+
return self.high_action
|
|
126
|
+
if severity_upper == "MEDIUM":
|
|
127
|
+
return self.medium_action
|
|
128
|
+
if severity_upper == "LOW":
|
|
129
|
+
return self.low_action
|
|
130
|
+
return EnforcementAction.LOG
|
|
131
|
+
|
|
132
|
+
@beartype
|
|
133
|
+
@ensure(lambda result: isinstance(result, dict), "Must return dictionary")
|
|
134
|
+
@ensure(
|
|
135
|
+
lambda result: all(isinstance(k, str) and isinstance(v, str) for k, v in result.items()),
|
|
136
|
+
"All keys and values must be strings",
|
|
137
|
+
)
|
|
138
|
+
@ensure(lambda result: set(result.keys()) == {"HIGH", "MEDIUM", "LOW"}, "Must have all three severity levels")
|
|
139
|
+
def to_summary_dict(self) -> dict[str, str]:
|
|
140
|
+
"""
|
|
141
|
+
Convert config to a summary dictionary for display.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Dictionary mapping severity to action
|
|
145
|
+
"""
|
|
146
|
+
return {
|
|
147
|
+
"HIGH": self.high_action.value,
|
|
148
|
+
"MEDIUM": self.medium_action.value,
|
|
149
|
+
"LOW": self.low_action.value,
|
|
150
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plan bundle data models.
|
|
3
|
+
|
|
4
|
+
This module defines Pydantic models for development plans, features,
|
|
5
|
+
and stories following the CLI-First specification.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Story(BaseModel):
|
|
16
|
+
"""User story model following Scrum/Agile practices."""
|
|
17
|
+
|
|
18
|
+
key: str = Field(..., description="Story key (e.g., STORY-001)")
|
|
19
|
+
title: str = Field(..., description="Story title (user-facing value statement)")
|
|
20
|
+
acceptance: list[str] = Field(default_factory=list, description="Acceptance criteria")
|
|
21
|
+
tags: list[str] = Field(default_factory=list, description="Story tags")
|
|
22
|
+
story_points: int | None = Field(None, ge=0, le=100, description="Story points (complexity: 1,2,3,5,8,13,21...)")
|
|
23
|
+
value_points: int | None = Field(
|
|
24
|
+
None, ge=0, le=100, description="Value points (business value: 1,2,3,5,8,13,21...)"
|
|
25
|
+
)
|
|
26
|
+
tasks: list[str] = Field(default_factory=list, description="Implementation tasks (methods, functions)")
|
|
27
|
+
confidence: float = Field(default=1.0, ge=0.0, le=1.0, description="Confidence score (0.0-1.0)")
|
|
28
|
+
draft: bool = Field(default=False, description="Whether this is a draft story")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Feature(BaseModel):
|
|
32
|
+
"""Feature model."""
|
|
33
|
+
|
|
34
|
+
key: str = Field(..., description="Feature key (e.g., FEATURE-001)")
|
|
35
|
+
title: str = Field(..., description="Feature title")
|
|
36
|
+
outcomes: list[str] = Field(default_factory=list, description="Expected outcomes")
|
|
37
|
+
acceptance: list[str] = Field(default_factory=list, description="Acceptance criteria")
|
|
38
|
+
constraints: list[str] = Field(default_factory=list, description="Constraints")
|
|
39
|
+
stories: list[Story] = Field(default_factory=list, description="User stories")
|
|
40
|
+
confidence: float = Field(default=1.0, ge=0.0, le=1.0, description="Confidence score (0.0-1.0)")
|
|
41
|
+
draft: bool = Field(default=False, description="Whether this is a draft feature")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Release(BaseModel):
|
|
45
|
+
"""Release model."""
|
|
46
|
+
|
|
47
|
+
name: str = Field(..., description="Release name")
|
|
48
|
+
objectives: list[str] = Field(default_factory=list, description="Release objectives")
|
|
49
|
+
scope: list[str] = Field(default_factory=list, description="Features in scope")
|
|
50
|
+
risks: list[str] = Field(default_factory=list, description="Release risks")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Product(BaseModel):
|
|
54
|
+
"""Product definition model."""
|
|
55
|
+
|
|
56
|
+
themes: list[str] = Field(default_factory=list, description="Product themes")
|
|
57
|
+
releases: list[Release] = Field(default_factory=list, description="Product releases")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Business(BaseModel):
|
|
61
|
+
"""Business context model."""
|
|
62
|
+
|
|
63
|
+
segments: list[str] = Field(default_factory=list, description="Market segments")
|
|
64
|
+
problems: list[str] = Field(default_factory=list, description="Problems being solved")
|
|
65
|
+
solutions: list[str] = Field(default_factory=list, description="Proposed solutions")
|
|
66
|
+
differentiation: list[str] = Field(default_factory=list, description="Differentiation points")
|
|
67
|
+
risks: list[str] = Field(default_factory=list, description="Business risks")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Idea(BaseModel):
|
|
71
|
+
"""Initial idea model."""
|
|
72
|
+
|
|
73
|
+
title: str = Field(..., description="Idea title")
|
|
74
|
+
narrative: str = Field(..., description="Idea narrative")
|
|
75
|
+
target_users: list[str] = Field(default_factory=list, description="Target user personas")
|
|
76
|
+
value_hypothesis: str = Field(default="", description="Value hypothesis")
|
|
77
|
+
constraints: list[str] = Field(default_factory=list, description="Idea constraints")
|
|
78
|
+
metrics: dict[str, Any] | None = Field(None, description="Success metrics")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Metadata(BaseModel):
|
|
82
|
+
"""Plan bundle metadata."""
|
|
83
|
+
|
|
84
|
+
stage: str = Field(default="draft", description="Plan stage (draft, review, approved, released)")
|
|
85
|
+
promoted_at: str | None = Field(None, description="ISO timestamp of last promotion")
|
|
86
|
+
promoted_by: str | None = Field(None, description="User who performed last promotion")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class PlanBundle(BaseModel):
|
|
90
|
+
"""Complete plan bundle model."""
|
|
91
|
+
|
|
92
|
+
version: str = Field(default="1.0", description="Plan bundle version")
|
|
93
|
+
idea: Idea | None = Field(None, description="Initial idea")
|
|
94
|
+
business: Business | None = Field(None, description="Business context")
|
|
95
|
+
product: Product = Field(..., description="Product definition")
|
|
96
|
+
features: list[Feature] = Field(default_factory=list, description="Product features")
|
|
97
|
+
metadata: Metadata | None = Field(None, description="Plan bundle metadata")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Protocol data models.
|
|
3
|
+
|
|
4
|
+
This module defines Pydantic models for FSM protocols, states,
|
|
5
|
+
and transitions following the CLI-First specification.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Transition(BaseModel):
|
|
14
|
+
"""State machine transition."""
|
|
15
|
+
|
|
16
|
+
from_state: str = Field(..., description="Source state")
|
|
17
|
+
on_event: str = Field(..., description="Triggering event")
|
|
18
|
+
to_state: str = Field(..., description="Target state")
|
|
19
|
+
guard: str | None = Field(None, description="Guard function name")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Protocol(BaseModel):
|
|
23
|
+
"""FSM protocol definition."""
|
|
24
|
+
|
|
25
|
+
states: list[str] = Field(..., description="State names")
|
|
26
|
+
start: str = Field(..., description="Initial state")
|
|
27
|
+
transitions: list[Transition] = Field(..., description="State transitions")
|
|
28
|
+
guards: dict[str, str] = Field(default_factory=dict, description="Guard definitions")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mode detection and routing for SpecFact CLI.
|
|
3
|
+
|
|
4
|
+
This package provides operational mode detection (CI/CD vs CoPilot) and command routing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from specfact_cli.modes.detector import OperationalMode, detect_mode
|
|
10
|
+
from specfact_cli.modes.router import CommandRouter, RoutingResult, get_router
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"CommandRouter",
|
|
14
|
+
"OperationalMode",
|
|
15
|
+
"RoutingResult",
|
|
16
|
+
"detect_mode",
|
|
17
|
+
"get_router",
|
|
18
|
+
]
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mode detection for SpecFact CLI.
|
|
3
|
+
|
|
4
|
+
This module provides automatic detection of operational mode (CI/CD vs CoPilot)
|
|
5
|
+
based on environment and explicit flags.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from enum import Enum
|
|
12
|
+
|
|
13
|
+
from beartype import beartype
|
|
14
|
+
from icontract import ensure, require
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OperationalMode(str, Enum):
|
|
18
|
+
"""Operational modes for SpecFact CLI."""
|
|
19
|
+
|
|
20
|
+
CICD = "cicd"
|
|
21
|
+
COPILOT = "copilot"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@beartype
|
|
25
|
+
@require(lambda explicit_mode: explicit_mode is None or isinstance(explicit_mode, OperationalMode))
|
|
26
|
+
@ensure(lambda result: isinstance(result, OperationalMode))
|
|
27
|
+
def detect_mode(explicit_mode: OperationalMode | None = None) -> OperationalMode:
|
|
28
|
+
"""
|
|
29
|
+
Auto-detect operational mode or use explicit override.
|
|
30
|
+
|
|
31
|
+
Priority:
|
|
32
|
+
1. Explicit mode flag (highest)
|
|
33
|
+
2. CoPilot API availability
|
|
34
|
+
3. IDE integration (VS Code/Cursor with CoPilot)
|
|
35
|
+
4. Default to CI/CD mode
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
explicit_mode: Explicitly specified mode (overrides auto-detection)
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Detected or explicit operational mode
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ValueError: If explicit_mode is invalid OperationalMode value
|
|
45
|
+
"""
|
|
46
|
+
# 1. Check explicit flag (highest priority)
|
|
47
|
+
if explicit_mode is not None:
|
|
48
|
+
return explicit_mode
|
|
49
|
+
|
|
50
|
+
# 2. Check environment variable (SPECFACT_MODE)
|
|
51
|
+
env_mode = os.environ.get("SPECFACT_MODE", "").lower()
|
|
52
|
+
if env_mode == "copilot":
|
|
53
|
+
return OperationalMode.COPILOT
|
|
54
|
+
if env_mode == "cicd":
|
|
55
|
+
return OperationalMode.CICD
|
|
56
|
+
|
|
57
|
+
# 3. Check CoPilot API availability
|
|
58
|
+
if copilot_api_available():
|
|
59
|
+
return OperationalMode.COPILOT
|
|
60
|
+
|
|
61
|
+
# 4. Check IDE integration
|
|
62
|
+
if ide_detected() and ide_has_copilot():
|
|
63
|
+
return OperationalMode.COPILOT
|
|
64
|
+
|
|
65
|
+
# 5. Default to CI/CD
|
|
66
|
+
return OperationalMode.CICD
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@beartype
|
|
70
|
+
@ensure(lambda result: isinstance(result, bool))
|
|
71
|
+
def copilot_api_available() -> bool:
|
|
72
|
+
"""
|
|
73
|
+
Check if CoPilot API is available.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
True if CoPilot API is available, False otherwise
|
|
77
|
+
"""
|
|
78
|
+
# Check environment variables
|
|
79
|
+
if os.environ.get("COPILOT_API_URL"):
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
# Check for CoPilot token or credentials
|
|
83
|
+
return bool(os.environ.get("COPILOT_API_TOKEN") or os.environ.get("GITHUB_COPILOT_TOKEN"))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@beartype
|
|
87
|
+
@ensure(lambda result: isinstance(result, bool))
|
|
88
|
+
def ide_detected() -> bool:
|
|
89
|
+
"""
|
|
90
|
+
Check if running in IDE (VS Code/Cursor).
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
True if running in IDE, False otherwise
|
|
94
|
+
"""
|
|
95
|
+
# Check VS Code
|
|
96
|
+
if os.environ.get("VSCODE_PID") or os.environ.get("VSCODE_INJECTION"):
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
# Check Cursor
|
|
100
|
+
if os.environ.get("CURSOR_PID") or os.environ.get("CURSOR_MODE"):
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
# Check for common IDE environment variables
|
|
104
|
+
return os.environ.get("TERM_PROGRAM") == "vscode"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@beartype
|
|
108
|
+
@ensure(lambda result: isinstance(result, bool))
|
|
109
|
+
def ide_has_copilot() -> bool:
|
|
110
|
+
"""
|
|
111
|
+
Check if IDE has CoPilot extension enabled.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
True if IDE has CoPilot enabled, False otherwise
|
|
115
|
+
"""
|
|
116
|
+
# Check for CoPilot extension environment variables
|
|
117
|
+
if os.environ.get("COPILOT_ENABLED") == "true":
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
# Check for VS Code/Cursor Copilot settings
|
|
121
|
+
if os.environ.get("VSCODE_COPILOT_ENABLED") == "true":
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
# Placeholder: Future implementation may check IDE configuration files
|
|
125
|
+
# For now, we only check environment variables
|
|
126
|
+
return os.environ.get("CURSOR_COPILOT_ENABLED") == "true"
|