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.
Files changed (63) hide show
  1. devflow/__init__.py +0 -0
  2. devflow/adapters/__init__.py +47 -0
  3. devflow/adapters/agents.py +49 -0
  4. devflow/adapters/base.py +81 -0
  5. devflow/adapters/claude_code.py +120 -0
  6. devflow/adapters/codex.py +111 -0
  7. devflow/adapters/detector.py +118 -0
  8. devflow/adapters/instructions.py +263 -0
  9. devflow/adapters/kiro.py +211 -0
  10. devflow/adapters/opencode.py +237 -0
  11. devflow/adapters/registry.py +41 -0
  12. devflow/cli.py +39 -0
  13. devflow/commands/__init__.py +6 -0
  14. devflow/commands/approve.py +98 -0
  15. devflow/commands/harness.py +98 -0
  16. devflow/commands/init.py +544 -0
  17. devflow/commands/spec.py +514 -0
  18. devflow/commands/status.py +156 -0
  19. devflow/core/__init__.py +15 -0
  20. devflow/core/architecture.py +469 -0
  21. devflow/core/artifacts/__init__.py +29 -0
  22. devflow/core/artifacts/errors.py +19 -0
  23. devflow/core/artifacts/models.py +82 -0
  24. devflow/core/artifacts/store.py +130 -0
  25. devflow/core/clock.py +67 -0
  26. devflow/core/config.py +706 -0
  27. devflow/core/constitution.py +166 -0
  28. devflow/core/discovery.py +1304 -0
  29. devflow/core/ports.py +159 -0
  30. devflow/core/profile_manager.py +311 -0
  31. devflow/core/project.py +533 -0
  32. devflow/core/spec_workflow.py +932 -0
  33. devflow/core/specification.py +2443 -0
  34. devflow/core/state/__init__.py +75 -0
  35. devflow/core/state/errors.py +39 -0
  36. devflow/core/state/events.py +725 -0
  37. devflow/core/state/locator.py +116 -0
  38. devflow/core/state/lock.py +69 -0
  39. devflow/core/state/manager.py +960 -0
  40. devflow/core/state/models.py +164 -0
  41. devflow/core/state/transitions.py +114 -0
  42. devflow/models/__init__.py +6 -0
  43. devflow/models/profile.py +63 -0
  44. devflow/models/project.py +47 -0
  45. devflow/profiles/ai-rag/constitution/ai-rag-standards.md +49 -0
  46. devflow/profiles/ai-rag/profile.yaml +28 -0
  47. devflow/profiles/base/constitution/architecture.md +58 -0
  48. devflow/profiles/base/constitution/code-standards.md +85 -0
  49. devflow/profiles/base/constitution/constitution.md +85 -0
  50. devflow/profiles/base/profile.yaml +15 -0
  51. devflow/profiles/django-backend/constitution/django-standards.md +64 -0
  52. devflow/profiles/django-backend/profile.yaml +24 -0
  53. devflow/profiles/fastapi-backend/constitution/fastapi-standards.md +59 -0
  54. devflow/profiles/fastapi-backend/profile.yaml +23 -0
  55. devflow/prompts/__init__.py +200 -0
  56. devflow/prompts/discovery.md +119 -0
  57. devflow/prompts/spec.greenfield.md +149 -0
  58. devflow/prompts/spec.md +116 -0
  59. devflow_toolkit-0.3.0.dist-info/METADATA +628 -0
  60. devflow_toolkit-0.3.0.dist-info/RECORD +63 -0
  61. devflow_toolkit-0.3.0.dist-info/WHEEL +4 -0
  62. devflow_toolkit-0.3.0.dist-info/entry_points.txt +2 -0
  63. devflow_toolkit-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,130 @@
1
+ """Atomic artifact storage with normalized SHA-256 digests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import os
7
+ import tempfile
8
+ from pathlib import Path
9
+
10
+ from devflow.core.artifacts.errors import (
11
+ ArtifactError,
12
+ ArtifactNotFoundError,
13
+ ArtifactOutsideProjectError,
14
+ ArtifactWriteError,
15
+ )
16
+
17
+
18
+ class ArtifactStore:
19
+ """Read, write and verify project artifacts.
20
+
21
+ Text is normalized before it is written and before its digest is
22
+ computed. The digest therefore identifies the stored artifact, not a
23
+ temporary prompt or a raw agent response.
24
+ """
25
+
26
+ def __init__(self, root: Path) -> None:
27
+ self.root = root.resolve()
28
+
29
+ def normalize(self, content: str) -> str:
30
+ """Return the canonical stored form of a text artifact."""
31
+ text = content.lstrip("\ufeff")
32
+ text = text.replace("\r\n", "\n").replace("\r", "\n")
33
+ return text.rstrip() + "\n"
34
+
35
+ def digest_text(self, content: str) -> str:
36
+ """Return the SHA-256 digest of normalized text."""
37
+ normalized = self.normalize(content)
38
+ digest = hashlib.sha256(normalized.encode("utf-8")).hexdigest()
39
+ return f"sha256:{digest}"
40
+
41
+ def digest_file(self, path: Path) -> str:
42
+ """Return the digest of a stored artifact."""
43
+ return self.digest_text(self.read(path))
44
+
45
+ def read(self, path: Path) -> str:
46
+ """Read an artifact as normalized text."""
47
+ target = self._inside_project(path)
48
+ if not target.is_file():
49
+ raise ArtifactNotFoundError(
50
+ f"Artifact does not exist: {self.display_path(target)}"
51
+ )
52
+ try:
53
+ content = target.read_text(encoding="utf-8")
54
+ except UnicodeDecodeError as error:
55
+ msg = f"Artifact is not valid UTF-8: {self.display_path(target)}"
56
+ raise ArtifactError(msg) from error
57
+ except OSError as error:
58
+ msg = f"Could not read artifact: {self.display_path(target)}"
59
+ raise ArtifactError(msg) from error
60
+ return self.normalize(content)
61
+
62
+ def write(
63
+ self,
64
+ path: Path,
65
+ content: str,
66
+ *,
67
+ archive: bool = False,
68
+ ) -> str:
69
+ """Write normalized text atomically and return its digest."""
70
+ target = self._inside_project(path)
71
+ normalized = self.normalize(content)
72
+ target.parent.mkdir(parents=True, exist_ok=True)
73
+ if archive and target.is_file():
74
+ self.archive(target)
75
+
76
+ descriptor, temporary_name = tempfile.mkstemp(
77
+ prefix=f".{target.name}-",
78
+ suffix=".tmp",
79
+ dir=target.parent,
80
+ )
81
+ temporary = Path(temporary_name)
82
+ try:
83
+ with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as handle:
84
+ handle.write(normalized)
85
+ handle.flush()
86
+ os.fsync(handle.fileno())
87
+ temporary.replace(target)
88
+ except OSError as error:
89
+ msg = f"Could not write artifact: {self.display_path(target)}"
90
+ raise ArtifactWriteError(msg) from error
91
+ finally:
92
+ temporary.unlink(missing_ok=True)
93
+ return self.digest_text(normalized)
94
+
95
+ def verify(self, path: Path, expected_digest: str) -> bool:
96
+ """Return whether the stored artifact still matches a digest."""
97
+ try:
98
+ return self.digest_file(path) == expected_digest
99
+ except ArtifactError:
100
+ return False
101
+
102
+ def archive(self, path: Path) -> Path:
103
+ """Copy the current artifact into its history directory."""
104
+ target = self._inside_project(path)
105
+ content = self.read(target)
106
+ digest = self.digest_text(content)
107
+ short = digest.removeprefix("sha256:")[:12]
108
+ history = target.parent / "history"
109
+ history.mkdir(parents=True, exist_ok=True)
110
+ archived = history / f"{short}-{target.name}"
111
+ if not archived.is_file():
112
+ self.write(archived, content)
113
+ return archived
114
+
115
+ def display_path(self, path: Path) -> str:
116
+ """Return a project-relative path for messages."""
117
+ target = path.resolve()
118
+ try:
119
+ return target.relative_to(self.root).as_posix()
120
+ except ValueError:
121
+ return target.as_posix()
122
+
123
+ def _inside_project(self, path: Path) -> Path:
124
+ target = path.resolve()
125
+ try:
126
+ target.relative_to(self.root)
127
+ except ValueError as error:
128
+ msg = f"Artifact path escapes the project root: {target}"
129
+ raise ArtifactOutsideProjectError(msg) from error
130
+ return target
devflow/core/clock.py ADDED
@@ -0,0 +1,67 @@
1
+ """Small runtime contracts used by state and artifact components."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from datetime import datetime, timezone
7
+ from typing import Protocol
8
+
9
+
10
+ class Clock(Protocol):
11
+ """Provides the current time."""
12
+
13
+ def now(self) -> datetime:
14
+ """Return the current UTC datetime."""
15
+
16
+
17
+ class SystemClock:
18
+ """Clock backed by the system clock."""
19
+
20
+ def now(self) -> datetime:
21
+ return datetime.now(timezone.utc)
22
+
23
+
24
+ class FixedClock:
25
+ """Deterministic clock for tests."""
26
+
27
+ def __init__(self, value: datetime) -> None:
28
+ self.value = value
29
+
30
+ def now(self) -> datetime:
31
+ return self.value
32
+
33
+
34
+ class IdGenerator(Protocol):
35
+ """Generates stable identifiers for events and runs."""
36
+
37
+ def new_id(self, prefix: str) -> str:
38
+ """Return a new identifier with the given prefix."""
39
+
40
+
41
+ class UuidIdGenerator:
42
+ """Identifier generator backed by UUID4."""
43
+
44
+ def new_id(self, prefix: str) -> str:
45
+ return f"{prefix}-{uuid.uuid4().hex[:12]}"
46
+
47
+
48
+ class SequentialIdGenerator:
49
+ """Deterministic identifier generator for tests."""
50
+
51
+ def __init__(self) -> None:
52
+ self._counters: dict[str, int] = {}
53
+
54
+ def new_id(self, prefix: str) -> str:
55
+ current = self._counters.get(prefix, 0) + 1
56
+ self._counters[prefix] = current
57
+ return f"{prefix}-{current:03d}"
58
+
59
+
60
+ def format_timestamp(value: datetime) -> str:
61
+ """Format a datetime as a stable UTC ISO-8601 timestamp."""
62
+ normalized = value
63
+ if normalized.tzinfo is None:
64
+ normalized = normalized.replace(tzinfo=timezone.utc)
65
+ else:
66
+ normalized = normalized.astimezone(timezone.utc)
67
+ return normalized.isoformat(timespec="seconds").replace("+00:00", "Z")