devflow-toolkit 0.3.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.
- devflow/__init__.py +0 -0
- devflow/adapters/__init__.py +47 -0
- devflow/adapters/agents.py +49 -0
- devflow/adapters/base.py +81 -0
- devflow/adapters/claude_code.py +120 -0
- devflow/adapters/codex.py +111 -0
- devflow/adapters/detector.py +118 -0
- devflow/adapters/instructions.py +263 -0
- devflow/adapters/kiro.py +211 -0
- devflow/adapters/opencode.py +237 -0
- devflow/adapters/registry.py +41 -0
- devflow/cli.py +39 -0
- devflow/commands/__init__.py +6 -0
- devflow/commands/approve.py +98 -0
- devflow/commands/harness.py +98 -0
- devflow/commands/init.py +544 -0
- devflow/commands/spec.py +514 -0
- devflow/commands/status.py +156 -0
- devflow/core/__init__.py +15 -0
- devflow/core/architecture.py +469 -0
- devflow/core/artifacts/__init__.py +29 -0
- devflow/core/artifacts/errors.py +19 -0
- devflow/core/artifacts/models.py +82 -0
- devflow/core/artifacts/store.py +130 -0
- devflow/core/clock.py +67 -0
- devflow/core/config.py +706 -0
- devflow/core/constitution.py +166 -0
- devflow/core/discovery.py +1304 -0
- devflow/core/ports.py +159 -0
- devflow/core/profile_manager.py +311 -0
- devflow/core/project.py +533 -0
- devflow/core/spec_workflow.py +932 -0
- devflow/core/specification.py +2443 -0
- devflow/core/state/__init__.py +75 -0
- devflow/core/state/errors.py +39 -0
- devflow/core/state/events.py +725 -0
- devflow/core/state/locator.py +116 -0
- devflow/core/state/lock.py +69 -0
- devflow/core/state/manager.py +960 -0
- devflow/core/state/models.py +164 -0
- devflow/core/state/transitions.py +114 -0
- devflow/models/__init__.py +6 -0
- devflow/models/profile.py +63 -0
- devflow/models/project.py +47 -0
- devflow/profiles/ai-rag/constitution/ai-rag-standards.md +49 -0
- devflow/profiles/ai-rag/profile.yaml +28 -0
- devflow/profiles/base/constitution/architecture.md +58 -0
- devflow/profiles/base/constitution/code-standards.md +85 -0
- devflow/profiles/base/constitution/constitution.md +85 -0
- devflow/profiles/base/profile.yaml +15 -0
- devflow/profiles/django-backend/constitution/django-standards.md +64 -0
- devflow/profiles/django-backend/profile.yaml +24 -0
- devflow/profiles/fastapi-backend/constitution/fastapi-standards.md +59 -0
- devflow/profiles/fastapi-backend/profile.yaml +23 -0
- devflow/prompts/__init__.py +200 -0
- devflow/prompts/discovery.md +119 -0
- devflow/prompts/spec.greenfield.md +149 -0
- devflow/prompts/spec.md +116 -0
- devflow_toolkit-0.3.0.dist-info/METADATA +628 -0
- devflow_toolkit-0.3.0.dist-info/RECORD +63 -0
- devflow_toolkit-0.3.0.dist-info/WHEEL +4 -0
- devflow_toolkit-0.3.0.dist-info/entry_points.txt +2 -0
- devflow_toolkit-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Loading the project constitution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from devflow.core.config import (
|
|
9
|
+
ConfigError,
|
|
10
|
+
DevFlowConfig,
|
|
11
|
+
validate_document_name,
|
|
12
|
+
)
|
|
13
|
+
from devflow.core.profile_manager import ProfileManager
|
|
14
|
+
from devflow.models.profile import Profile
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConstitutionError(Exception):
|
|
18
|
+
"""Raised when constitution documents cannot be loaded."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _document_name(filename: str) -> str:
|
|
22
|
+
"""Validate a constitution document name for this loader."""
|
|
23
|
+
try:
|
|
24
|
+
return validate_document_name(filename, "constitution")
|
|
25
|
+
except ConfigError as error:
|
|
26
|
+
raise ConstitutionError(str(error)) from error
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _document_path(base_dir: Path, filename: str) -> Path:
|
|
30
|
+
"""Resolve a constitution document without allowing path escapes."""
|
|
31
|
+
name = _document_name(filename)
|
|
32
|
+
base = base_dir.resolve()
|
|
33
|
+
candidate = (base / name).resolve()
|
|
34
|
+
try:
|
|
35
|
+
candidate.relative_to(base)
|
|
36
|
+
except ValueError as error:
|
|
37
|
+
msg = (
|
|
38
|
+
"Constitution document must stay inside the constitution "
|
|
39
|
+
f"directory: {filename}"
|
|
40
|
+
)
|
|
41
|
+
raise ConstitutionError(msg) from error
|
|
42
|
+
return candidate
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class ConstitutionDocument:
|
|
47
|
+
"""A single constitution document."""
|
|
48
|
+
|
|
49
|
+
name: str
|
|
50
|
+
path: Path
|
|
51
|
+
content: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class Constitution:
|
|
56
|
+
"""The set of guidelines governing a project."""
|
|
57
|
+
|
|
58
|
+
documents: tuple[ConstitutionDocument, ...]
|
|
59
|
+
profiles: tuple[Profile, ...]
|
|
60
|
+
warnings: tuple[str, ...] = ()
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def names(self) -> tuple[str, ...]:
|
|
64
|
+
"""Return the loaded document names."""
|
|
65
|
+
return tuple(document.name for document in self.documents)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ConstitutionLoader:
|
|
69
|
+
"""Load base and profile-specific constitution documents."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, profile_manager: ProfileManager | None = None) -> None:
|
|
72
|
+
self.profile_manager = profile_manager or ProfileManager()
|
|
73
|
+
|
|
74
|
+
def load(
|
|
75
|
+
self,
|
|
76
|
+
config: DevFlowConfig,
|
|
77
|
+
profiles: list[Profile],
|
|
78
|
+
) -> Constitution:
|
|
79
|
+
"""Load configured and profile-specific constitution documents."""
|
|
80
|
+
base_dir = config.constitution.path
|
|
81
|
+
if not base_dir.is_dir():
|
|
82
|
+
msg = (
|
|
83
|
+
f"Constitution directory not found: {base_dir}. "
|
|
84
|
+
"Run 'devflow init' to restore the guidelines."
|
|
85
|
+
)
|
|
86
|
+
raise ConstitutionError(msg)
|
|
87
|
+
|
|
88
|
+
names: list[str] = [
|
|
89
|
+
_document_name(filename) for filename in config.constitution.files
|
|
90
|
+
]
|
|
91
|
+
warnings: list[str] = []
|
|
92
|
+
|
|
93
|
+
selected_profiles: list[Profile] = []
|
|
94
|
+
if config.constitution.base:
|
|
95
|
+
selected_profiles.extend(
|
|
96
|
+
profile for profile in profiles if profile.name == "base"
|
|
97
|
+
)
|
|
98
|
+
if config.constitution.profile_specific:
|
|
99
|
+
selected_profiles.extend(
|
|
100
|
+
profile for profile in profiles if profile.name != "base"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
for filename in self.profile_manager.constitution_documents(
|
|
104
|
+
selected_profiles
|
|
105
|
+
):
|
|
106
|
+
name = _document_name(filename)
|
|
107
|
+
if name not in names:
|
|
108
|
+
candidate = _document_path(base_dir, name)
|
|
109
|
+
if candidate.is_file():
|
|
110
|
+
names.append(name)
|
|
111
|
+
else:
|
|
112
|
+
warnings.append(
|
|
113
|
+
f"Profile guideline is not installed: {name}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if not names:
|
|
117
|
+
for path in sorted(base_dir.glob("*.md")):
|
|
118
|
+
try:
|
|
119
|
+
candidate = _document_path(base_dir, path.name)
|
|
120
|
+
except ConstitutionError:
|
|
121
|
+
warnings.append(
|
|
122
|
+
"Skipped constitution document outside "
|
|
123
|
+
f"{base_dir}: {path.name}"
|
|
124
|
+
)
|
|
125
|
+
continue
|
|
126
|
+
if candidate.is_file():
|
|
127
|
+
names.append(path.name)
|
|
128
|
+
|
|
129
|
+
if not names:
|
|
130
|
+
msg = f"No constitution documents found in {base_dir}."
|
|
131
|
+
raise ConstitutionError(msg)
|
|
132
|
+
|
|
133
|
+
documents: list[ConstitutionDocument] = []
|
|
134
|
+
seen: set[str] = set()
|
|
135
|
+
for filename in names:
|
|
136
|
+
if filename in seen:
|
|
137
|
+
continue
|
|
138
|
+
seen.add(filename)
|
|
139
|
+
|
|
140
|
+
path = _document_path(base_dir, filename)
|
|
141
|
+
if not path.is_file():
|
|
142
|
+
msg = (
|
|
143
|
+
f"Constitution document not found: {filename}. "
|
|
144
|
+
"Run 'devflow init' or update .devflow/config.yaml."
|
|
145
|
+
)
|
|
146
|
+
raise ConstitutionError(msg)
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
content = path.read_text(encoding="utf-8")
|
|
150
|
+
except OSError as error:
|
|
151
|
+
msg = f"Could not read constitution document: {path}"
|
|
152
|
+
raise ConstitutionError(msg) from error
|
|
153
|
+
|
|
154
|
+
documents.append(
|
|
155
|
+
ConstitutionDocument(
|
|
156
|
+
name=filename,
|
|
157
|
+
path=path,
|
|
158
|
+
content=content,
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return Constitution(
|
|
163
|
+
documents=tuple(documents),
|
|
164
|
+
profiles=tuple(profiles),
|
|
165
|
+
warnings=tuple(warnings),
|
|
166
|
+
)
|