atdd 0.4.2__py3-none-any.whl → 0.4.4__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.
@@ -169,6 +169,10 @@ class ProjectInitializer:
169
169
 
170
170
  config = {
171
171
  "version": "1.0",
172
+ "release": {
173
+ "version_file": "VERSION",
174
+ "tag_prefix": "v",
175
+ },
172
176
  "sync": {
173
177
  "agents": ["claude"], # Default: only Claude
174
178
  },
@@ -194,6 +194,11 @@ class AgentConfigSync:
194
194
  """
195
195
  agents = self._get_enabled_agents()
196
196
 
197
+ # Get configured vs detected for display
198
+ config = self._load_config()
199
+ sync_config = config.get("sync", {})
200
+ configured_agents = set(sync_config.get("agents", []))
201
+
197
202
  print("\n" + "=" * 60)
198
203
  print("ATDD Agent Config Sync Status")
199
204
  print("=" * 60)
@@ -202,8 +207,8 @@ class AgentConfigSync:
202
207
  print(f"ATDD template: {self.atdd_template}")
203
208
  print(f"Overlays dir: {self.overlays_dir}")
204
209
 
205
- print(f"\n{'Agent':<10} {'File':<15} {'Status':<20}")
206
- print("-" * 50)
210
+ print(f"\n{'Agent':<10} {'File':<15} {'Status':<20} {'Source':<12}")
211
+ print("-" * 62)
207
212
 
208
213
  for agent, target_file in sorted(self.AGENT_FILES.items()):
209
214
  target_path = self.target_dir / target_file
@@ -211,18 +216,22 @@ class AgentConfigSync:
211
216
 
212
217
  if not enabled:
213
218
  status = "disabled"
219
+ source = ""
214
220
  elif not target_path.exists():
215
221
  status = "missing"
222
+ source = "config"
216
223
  elif not self._has_managed_block(target_path.read_text()):
217
224
  status = "no managed block"
225
+ source = "auto" if agent not in configured_agents else "config"
218
226
  else:
219
227
  status = "synced"
228
+ source = "auto" if agent not in configured_agents else "config"
220
229
 
221
230
  enabled_marker = "*" if enabled else " "
222
- print(f"{enabled_marker} {agent:<8} {target_file:<15} {status:<20}")
231
+ print(f"{enabled_marker} {agent:<8} {target_file:<15} {status:<20} {source:<12}")
223
232
 
224
- print("-" * 50)
225
- print("* = enabled in config")
233
+ print("-" * 62)
234
+ print("* = enabled for sync (config = explicit, auto = file exists)")
226
235
 
227
236
  # Show overlay status
228
237
  print("\nOverlays:")
@@ -250,14 +259,32 @@ class AgentConfigSync:
250
259
 
251
260
  def _get_enabled_agents(self) -> List[str]:
252
261
  """
253
- Return agents from config.
262
+ Return agents to sync: configured agents + existing agent files.
263
+
264
+ Auto-includes any supported agent file that already exists in the
265
+ target directory, in addition to explicitly configured agents.
266
+ This ensures existing agent files stay in sync without requiring
267
+ explicit configuration.
254
268
 
255
269
  Returns:
256
- List of agent names enabled for sync.
270
+ List of unique agent names enabled for sync.
257
271
  """
272
+ # Get explicitly configured agents
258
273
  config = self._load_config()
259
274
  sync_config = config.get("sync", {})
260
- return sync_config.get("agents", [])
275
+ configured_agents = set(sync_config.get("agents", []))
276
+
277
+ # Auto-detect existing agent files
278
+ detected_agents = set()
279
+ for agent, filename in self.AGENT_FILES.items():
280
+ agent_path = self.target_dir / filename
281
+ if agent_path.exists():
282
+ detected_agents.add(agent)
283
+
284
+ # Merge: configured + detected
285
+ all_agents = configured_agents | detected_agents
286
+
287
+ return sorted(all_agents)
261
288
 
262
289
  def _load_base_content(self) -> Optional[str]:
263
290
  """
@@ -11,6 +11,25 @@
11
11
  "pattern": "^[0-9]+\\.[0-9]+$",
12
12
  "examples": ["1.0"]
13
13
  },
14
+ "release": {
15
+ "type": "object",
16
+ "description": "Release/versioning settings",
17
+ "properties": {
18
+ "version_file": {
19
+ "type": "string",
20
+ "description": "Path to version file (relative to repo root)",
21
+ "examples": ["pyproject.toml", "package.json", "VERSION"]
22
+ },
23
+ "tag_prefix": {
24
+ "type": "string",
25
+ "description": "Prefix for git tags",
26
+ "default": "v",
27
+ "examples": ["v", ""]
28
+ }
29
+ },
30
+ "required": ["version_file"],
31
+ "additionalProperties": false
32
+ },
14
33
  "sync": {
15
34
  "type": "object",
16
35
  "description": "Agent config file sync settings",
@@ -27,8 +46,20 @@
27
46
  }
28
47
  },
29
48
  "additionalProperties": false
49
+ },
50
+ "toolkit": {
51
+ "type": "object",
52
+ "description": "ATDD toolkit metadata",
53
+ "properties": {
54
+ "last_version": {
55
+ "type": "string",
56
+ "description": "Last installed ATDD toolkit version"
57
+ }
58
+ },
59
+ "required": ["last_version"],
60
+ "additionalProperties": false
30
61
  }
31
62
  },
32
- "required": ["version"],
63
+ "required": ["version", "release"],
33
64
  "additionalProperties": false
34
65
  }
@@ -225,7 +225,9 @@ release:
225
225
  mandatory: true
226
226
 
227
227
  rules:
228
+ - "Version file is required (configured in .atdd/config.yaml)"
228
229
  - "Tag must match version exactly: v{version}"
230
+ - "Tag must be on HEAD"
229
231
  - "No tag without version bump"
230
232
  - "No version bump without tag"
231
233
  - "Every repo MUST have versioning"
@@ -243,10 +245,11 @@ release:
243
245
  - "Push with tags: git push origin {branch} --tags"
244
246
  - "Record in Session Log: 'Released: v{version}'"
245
247
 
246
- # Consumer repos configure version file location in .atdd/config.yaml:
248
+ # Config (required in .atdd/config.yaml):
247
249
  # release:
248
250
  # version_file: "pyproject.toml" # or package.json, VERSION, etc.
249
251
  # tag_prefix: "v"
252
+ # Validator: atdd validate coach enforces version file + tag on HEAD
250
253
 
251
254
  # Agent Coordination (Detailed in action files)
252
255
  agents:
@@ -0,0 +1,178 @@
1
+ """
2
+ Release versioning validation.
3
+
4
+ Ensures:
5
+ - .atdd/config.yaml defines release.version_file
6
+ - Version file exists and contains a version
7
+ - Git tag on HEAD matches tag_prefix + version
8
+ """
9
+
10
+ import json
11
+ import re
12
+ import subprocess
13
+ from pathlib import Path
14
+ from typing import Optional
15
+
16
+ import pytest
17
+ import yaml
18
+
19
+ from atdd.coach.utils.repo import find_repo_root
20
+
21
+
22
+ REPO_ROOT = find_repo_root()
23
+ CONFIG_FILE = REPO_ROOT / ".atdd" / "config.yaml"
24
+
25
+
26
+ def _load_config() -> dict:
27
+ if not CONFIG_FILE.exists():
28
+ pytest.skip(f"Config not found: {CONFIG_FILE}. Run 'atdd init' first.")
29
+
30
+ with open(CONFIG_FILE) as f:
31
+ return yaml.safe_load(f) or {}
32
+
33
+
34
+ def _get_release_config(config: dict) -> tuple[str, str]:
35
+ release = config.get("release")
36
+ if not isinstance(release, dict):
37
+ pytest.fail(
38
+ "Missing release config in .atdd/config.yaml. "
39
+ "Add release.version_file and release.tag_prefix."
40
+ )
41
+
42
+ version_file = release.get("version_file")
43
+ if not version_file or not isinstance(version_file, str):
44
+ pytest.fail("Missing release.version_file in .atdd/config.yaml.")
45
+
46
+ tag_prefix = release.get("tag_prefix", "v")
47
+ if tag_prefix is None:
48
+ tag_prefix = ""
49
+ if not isinstance(tag_prefix, str):
50
+ pytest.fail("release.tag_prefix must be a string.")
51
+
52
+ return version_file, tag_prefix
53
+
54
+
55
+ def _read_version_from_file(path: Path) -> str:
56
+ if not path.exists():
57
+ pytest.fail(f"Version file not found: {path}")
58
+
59
+ if path.name == "pyproject.toml":
60
+ version = _parse_pyproject_version(path)
61
+ elif path.name == "package.json":
62
+ version = _parse_package_json_version(path)
63
+ else:
64
+ version = _parse_plain_version(path)
65
+
66
+ if not version:
67
+ pytest.fail(f"Could not read version from {path}")
68
+
69
+ return version
70
+
71
+
72
+ def _parse_pyproject_version(path: Path) -> Optional[str]:
73
+ text = path.read_text()
74
+
75
+ # Try tomllib/tomli first for correctness
76
+ data = _load_toml(text)
77
+ if isinstance(data, dict):
78
+ project = data.get("project", {})
79
+ if isinstance(project, dict) and project.get("version"):
80
+ return str(project["version"]).strip()
81
+ tool = data.get("tool", {})
82
+ if isinstance(tool, dict):
83
+ poetry = tool.get("poetry", {})
84
+ if isinstance(poetry, dict) and poetry.get("version"):
85
+ return str(poetry["version"]).strip()
86
+
87
+ # Fallback to lightweight parsing
88
+ return _parse_pyproject_version_text(text)
89
+
90
+
91
+ def _load_toml(text: str) -> Optional[dict]:
92
+ try:
93
+ import tomllib # type: ignore[attr-defined]
94
+ return tomllib.loads(text)
95
+ except Exception:
96
+ try:
97
+ import tomli # type: ignore
98
+ return tomli.loads(text)
99
+ except Exception:
100
+ return None
101
+
102
+
103
+ def _parse_pyproject_version_text(text: str) -> Optional[str]:
104
+ current_section = None
105
+ for line in text.splitlines():
106
+ stripped = line.strip()
107
+ if not stripped or stripped.startswith("#"):
108
+ continue
109
+ if stripped.startswith("[") and stripped.endswith("]"):
110
+ current_section = stripped.strip("[]").strip()
111
+ continue
112
+ if current_section in {"project", "tool.poetry"}:
113
+ match = re.match(r'version\s*=\s*["\']([^"\']+)["\']', stripped)
114
+ if match:
115
+ return match.group(1).strip()
116
+ return None
117
+
118
+
119
+ def _parse_package_json_version(path: Path) -> Optional[str]:
120
+ try:
121
+ data = json.loads(path.read_text())
122
+ except json.JSONDecodeError:
123
+ return None
124
+
125
+ version = data.get("version")
126
+ return str(version).strip() if version else None
127
+
128
+
129
+ def _parse_plain_version(path: Path) -> Optional[str]:
130
+ for line in path.read_text().splitlines():
131
+ stripped = line.strip()
132
+ if not stripped or stripped.startswith("#"):
133
+ continue
134
+ return stripped
135
+ return None
136
+
137
+
138
+ def _git_tags_on_head(repo_root: Path) -> list[str]:
139
+ result = subprocess.run(
140
+ ["git", "tag", "--points-at", "HEAD"],
141
+ cwd=str(repo_root),
142
+ capture_output=True,
143
+ text=True,
144
+ )
145
+ if result.returncode != 0:
146
+ stderr = result.stderr.strip() or "git tag --points-at HEAD failed"
147
+ pytest.fail(stderr)
148
+
149
+ tags = [line.strip() for line in result.stdout.splitlines() if line.strip()]
150
+ return tags
151
+
152
+
153
+ def test_release_version_file_and_tag_on_head():
154
+ """
155
+ SPEC-RELEASE-0001: Version file exists and tag on HEAD matches version.
156
+ """
157
+ config = _load_config()
158
+ version_file, tag_prefix = _get_release_config(config)
159
+
160
+ version_path = Path(version_file)
161
+ if not version_path.is_absolute():
162
+ version_path = (REPO_ROOT / version_path).resolve()
163
+
164
+ version = _read_version_from_file(version_path)
165
+ expected_tag = f"{tag_prefix}{version}"
166
+
167
+ tags = _git_tags_on_head(REPO_ROOT)
168
+ if not tags:
169
+ pytest.fail(
170
+ "No git tag found on HEAD. "
171
+ f"Create tag: git tag {expected_tag}"
172
+ )
173
+
174
+ if expected_tag not in tags:
175
+ found = ", ".join(tags) if tags else "none"
176
+ pytest.fail(
177
+ f"Expected tag '{expected_tag}' on HEAD, found: {found}"
178
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atdd
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: ATDD Platform - Acceptance Test Driven Development toolkit
5
5
  License: MIT
6
6
  Requires-Python: >=3.10
@@ -230,6 +230,20 @@ atdd validate --coverage # With coverage report
230
230
  atdd validate --html # With HTML report
231
231
  ```
232
232
 
233
+ ### Release Versioning
234
+
235
+ ATDD enforces release versioning via coach validators. Configure the version file and tag prefix in `.atdd/config.yaml`:
236
+
237
+ ```yaml
238
+ release:
239
+ version_file: "pyproject.toml" # or package.json, VERSION, etc.
240
+ tag_prefix: "v"
241
+ ```
242
+
243
+ Validation (`atdd validate coach` or `atdd validate`) requires:
244
+ - Version file exists and contains a version
245
+ - Git tag on HEAD matches `{tag_prefix}{version}`
246
+
233
247
  ### Other Commands
234
248
 
235
249
  ```bash
@@ -10,13 +10,13 @@ atdd/coach/commands/analyze_migrations.py,sha256=2bLfR7OwicBXAZWB4R3Sm4d5jFe87d0
10
10
  atdd/coach/commands/consumers.py,sha256=7vTexse8xznXSzNWjPGYuF4iJ8ZWymmOSkpcA6IWyxU,27514
11
11
  atdd/coach/commands/gate.py,sha256=_V2GypqoGixTs_kLWxFF3HgEt-Wi2r6Iv0YL75yWrWo,5829
12
12
  atdd/coach/commands/infer_governance_status.py,sha256=MlLnx8SrJAOQq2rfuxLZMmyNylCQ-OYx4tSi_iFdhRA,4504
13
- atdd/coach/commands/initializer.py,sha256=s7NnITF4c6xGlN6ZJVjyv4a4jxGMjK9OPG1C-OIG55A,6385
13
+ atdd/coach/commands/initializer.py,sha256=wuvzj7QwA11ilNjRZU6Bx2bLQXITdBHJxR9_mZK7xjA,6503
14
14
  atdd/coach/commands/interface.py,sha256=FwBrJpWkfSL9n4n0HT_EC-alseXgU0bweKD4TImyHN0,40483
15
15
  atdd/coach/commands/inventory.py,sha256=qU42MnkXt1JSBh5GU7pPSKmCO27Zfga7XwMT19RquJE,20969
16
16
  atdd/coach/commands/migration.py,sha256=wRxU7emvvHqWt1MvXKkNTkPBjp0sU9g8F5Uy5yV2YfI,8177
17
17
  atdd/coach/commands/registry.py,sha256=76-Pe3_cN483JR1pXUdDIE5WSZjWtVV0Jl8dRtRw_9Y,58349
18
18
  atdd/coach/commands/session.py,sha256=MhuWXd5TR6bB3w0t8vANeZx3L476qwLT6EUQMwg-wQA,14268
19
- atdd/coach/commands/sync.py,sha256=4HNzg8AVaePDwhyBlQcywvNVZDP5NIW8TBsEPbqmJuQ,12545
19
+ atdd/coach/commands/sync.py,sha256=SLNzhcc6IuzMofMbkH9wM9rBSk5tPfcWPKXn9TaSZ-Y,13782
20
20
  atdd/coach/commands/test_interface.py,sha256=a7ut2Hhk0PnQ5LfJZkoQwfkfkVuB5OHA4QBwOS0-jcg,16870
21
21
  atdd/coach/commands/test_runner.py,sha256=_6JrDRq5fBHUOC4MtkgXcjkgJjG80otoGRTqnkEphIk,5832
22
22
  atdd/coach/commands/traceability.py,sha256=8TmpZDeUVHJAz-p3oxXq55jCFiFpKIQR8h1wLZVYcgA,163612
@@ -25,9 +25,9 @@ atdd/coach/commands/tests/test_telemetry_array_validation.py,sha256=WK5ZXvR1avlz
25
25
  atdd/coach/conventions/session.convention.yaml,sha256=1wCxQ_Y2Wb2080Xt2JZs0_WsV8_4SC0Tq87G_BCGdiE,26049
26
26
  atdd/coach/overlays/__init__.py,sha256=2lMiMSgfLJ3YHLpbzNI5B88AdQxiMEwjIfsWWb8t3To,123
27
27
  atdd/coach/overlays/claude.md,sha256=33mhpqhmsRhCtdWlU7cMXAJDsaVra9uBBK8URV8OtQA,101
28
- atdd/coach/schemas/config.schema.json,sha256=xzct7gBoPTIGh3NFPSGtfW0zIiyFdHDZkvjuy1qgAqA,951
28
+ atdd/coach/schemas/config.schema.json,sha256=CpePppEAB6WiLeWVgWW3EKOxlLvMHHcWisRnJL9z_SE,1863
29
29
  atdd/coach/schemas/manifest.schema.json,sha256=WO13-YF_FgH1awh96khCtk-112b6XSC24anlY3B7GjY,2885
30
- atdd/coach/templates/ATDD.md,sha256=jovi7CeJKidboZPKLmge5OCNESq998dHPr8zTHRRvlg,13100
30
+ atdd/coach/templates/ATDD.md,sha256=MLbrVbCETJre4c05d5FXGuf6W95Hz9E0jpE4RI9r4cg,13237
31
31
  atdd/coach/templates/SESSION-TEMPLATE.md,sha256=gcmfDDD6rREI20vhWXlf01AGbRbR8Hh7Q4QZX4H-pVw,9455
32
32
  atdd/coach/utils/__init__.py,sha256=7Jbo-heJEKSAn6I0s35z_2S4R8qGZ48PL6a2IntcNYg,148
33
33
  atdd/coach/utils/repo.py,sha256=0kiF5WpVTen0nO14u5T0RflznZhgGco2i9CwKobOh38,3757
@@ -37,6 +37,7 @@ atdd/coach/validators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
37
37
  atdd/coach/validators/shared_fixtures.py,sha256=tdqAb4675P-oOCL08mvSCG9XpmwMCjL9iSq1W5U7-wk,12558
38
38
  atdd/coach/validators/test_enrich_wagon_registry.py,sha256=WeTwYJqoNY6mEYc-QAvQo7YVagSOjaNKxB6Q6dpWqIM,6561
39
39
  atdd/coach/validators/test_registry.py,sha256=ffN70yA_1xxL3R8gdpGbY2M8dQXyuajIZhBZ-ylNiNs,17845
40
+ atdd/coach/validators/test_release_versioning.py,sha256=-H2hCRfdikVP54LHNDqW9IDmm6JLNBUZRdnF2uICvOI,5194
40
41
  atdd/coach/validators/test_session_validation.py,sha256=0VszXtFwRTO04b5CxDPO3klk0VfiqlpdbNpshjMn-qU,39079
41
42
  atdd/coach/validators/test_traceability.py,sha256=qTyobt41VBiCr6xRN2C7BPtGYvk_2poVQIe814Blt8E,15977
42
43
  atdd/coach/validators/test_update_feature_paths.py,sha256=zOKVDgEIpncSJwDh_shyyou5Pu-Ai7Z_XgF8zAbQVTA,4528
@@ -181,9 +182,9 @@ atdd/tester/validators/test_red_supabase_layer_structure.py,sha256=zbUjsMWSJE1MP
181
182
  atdd/tester/validators/test_telemetry_structure.py,sha256=uU5frZnxSlOn60iHyqhe7Pg9b0wrOV7N14D4S6Aw6TE,22626
182
183
  atdd/tester/validators/test_typescript_test_naming.py,sha256=E-TyGv_GVlTfsbyuxrtv9sOWSZS_QcpH6rrJFbWoeeU,11280
183
184
  atdd/tester/validators/test_typescript_test_structure.py,sha256=eV89SD1RaKtchBZupqhnJmaruoROosf3LwB4Fwe4UJI,2612
184
- atdd-0.4.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
185
- atdd-0.4.2.dist-info/METADATA,sha256=u367DyQRsEduvaTko1XLLX7sfVQZSmvC8Gmrr5tRSVE,8013
186
- atdd-0.4.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
187
- atdd-0.4.2.dist-info/entry_points.txt,sha256=-C3yrA1WQQfN3iuGmSzPapA5cKVBEYU5Q1HUffSJTbY,38
188
- atdd-0.4.2.dist-info/top_level.txt,sha256=VKkf6Uiyrm4RS6ULCGM-v8AzYN8K2yg8SMqwJLoO-xs,5
189
- atdd-0.4.2.dist-info/RECORD,,
185
+ atdd-0.4.4.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
186
+ atdd-0.4.4.dist-info/METADATA,sha256=cBMNKGmx5Uyw5kwn_v6ezv-CKbuFd5QpsZ6hFG_rg9k,8426
187
+ atdd-0.4.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
188
+ atdd-0.4.4.dist-info/entry_points.txt,sha256=-C3yrA1WQQfN3iuGmSzPapA5cKVBEYU5Q1HUffSJTbY,38
189
+ atdd-0.4.4.dist-info/top_level.txt,sha256=VKkf6Uiyrm4RS6ULCGM-v8AzYN8K2yg8SMqwJLoO-xs,5
190
+ atdd-0.4.4.dist-info/RECORD,,
File without changes