atdd 0.1.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 (183) hide show
  1. atdd/__init__.py +0 -0
  2. atdd/cli.py +404 -0
  3. atdd/coach/__init__.py +0 -0
  4. atdd/coach/commands/__init__.py +0 -0
  5. atdd/coach/commands/add_persistence_metadata.py +215 -0
  6. atdd/coach/commands/analyze_migrations.py +188 -0
  7. atdd/coach/commands/consumers.py +720 -0
  8. atdd/coach/commands/infer_governance_status.py +149 -0
  9. atdd/coach/commands/initializer.py +177 -0
  10. atdd/coach/commands/interface.py +1078 -0
  11. atdd/coach/commands/inventory.py +565 -0
  12. atdd/coach/commands/migration.py +240 -0
  13. atdd/coach/commands/registry.py +1560 -0
  14. atdd/coach/commands/session.py +430 -0
  15. atdd/coach/commands/sync.py +405 -0
  16. atdd/coach/commands/test_interface.py +399 -0
  17. atdd/coach/commands/test_runner.py +141 -0
  18. atdd/coach/commands/tests/__init__.py +1 -0
  19. atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
  20. atdd/coach/commands/traceability.py +4264 -0
  21. atdd/coach/conventions/session.convention.yaml +754 -0
  22. atdd/coach/overlays/__init__.py +2 -0
  23. atdd/coach/overlays/claude.md +2 -0
  24. atdd/coach/schemas/config.schema.json +34 -0
  25. atdd/coach/schemas/manifest.schema.json +101 -0
  26. atdd/coach/templates/ATDD.md +282 -0
  27. atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
  28. atdd/coach/utils/__init__.py +0 -0
  29. atdd/coach/utils/graph/__init__.py +0 -0
  30. atdd/coach/utils/graph/urn.py +875 -0
  31. atdd/coach/validators/__init__.py +0 -0
  32. atdd/coach/validators/shared_fixtures.py +365 -0
  33. atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
  34. atdd/coach/validators/test_registry.py +575 -0
  35. atdd/coach/validators/test_session_validation.py +1183 -0
  36. atdd/coach/validators/test_traceability.py +448 -0
  37. atdd/coach/validators/test_update_feature_paths.py +108 -0
  38. atdd/coach/validators/test_validate_contract_consumers.py +297 -0
  39. atdd/coder/__init__.py +1 -0
  40. atdd/coder/conventions/adapter.recipe.yaml +88 -0
  41. atdd/coder/conventions/backend.convention.yaml +460 -0
  42. atdd/coder/conventions/boundaries.convention.yaml +666 -0
  43. atdd/coder/conventions/commons.convention.yaml +460 -0
  44. atdd/coder/conventions/complexity.recipe.yaml +109 -0
  45. atdd/coder/conventions/component-naming.convention.yaml +178 -0
  46. atdd/coder/conventions/design.convention.yaml +327 -0
  47. atdd/coder/conventions/design.recipe.yaml +273 -0
  48. atdd/coder/conventions/dto.convention.yaml +660 -0
  49. atdd/coder/conventions/frontend.convention.yaml +542 -0
  50. atdd/coder/conventions/green.convention.yaml +1012 -0
  51. atdd/coder/conventions/presentation.convention.yaml +587 -0
  52. atdd/coder/conventions/refactor.convention.yaml +535 -0
  53. atdd/coder/conventions/technology.convention.yaml +206 -0
  54. atdd/coder/conventions/tests/__init__.py +0 -0
  55. atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
  56. atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
  57. atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
  58. atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
  59. atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
  60. atdd/coder/conventions/thinness.recipe.yaml +82 -0
  61. atdd/coder/conventions/train.convention.yaml +325 -0
  62. atdd/coder/conventions/verification.protocol.yaml +53 -0
  63. atdd/coder/schemas/design_system.schema.json +361 -0
  64. atdd/coder/validators/__init__.py +0 -0
  65. atdd/coder/validators/test_commons_structure.py +485 -0
  66. atdd/coder/validators/test_complexity.py +416 -0
  67. atdd/coder/validators/test_cross_language_consistency.py +431 -0
  68. atdd/coder/validators/test_design_system_compliance.py +413 -0
  69. atdd/coder/validators/test_dto_testing_patterns.py +268 -0
  70. atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
  71. atdd/coder/validators/test_green_layer_dependencies.py +148 -0
  72. atdd/coder/validators/test_green_python_layer_structure.py +103 -0
  73. atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
  74. atdd/coder/validators/test_import_boundaries.py +396 -0
  75. atdd/coder/validators/test_init_file_urns.py +593 -0
  76. atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
  77. atdd/coder/validators/test_presentation_convention.py +260 -0
  78. atdd/coder/validators/test_python_architecture.py +674 -0
  79. atdd/coder/validators/test_quality_metrics.py +420 -0
  80. atdd/coder/validators/test_station_master_pattern.py +244 -0
  81. atdd/coder/validators/test_train_infrastructure.py +454 -0
  82. atdd/coder/validators/test_train_urns.py +293 -0
  83. atdd/coder/validators/test_typescript_architecture.py +616 -0
  84. atdd/coder/validators/test_usecase_structure.py +421 -0
  85. atdd/coder/validators/test_wagon_boundaries.py +586 -0
  86. atdd/conftest.py +126 -0
  87. atdd/planner/__init__.py +1 -0
  88. atdd/planner/conventions/acceptance.convention.yaml +538 -0
  89. atdd/planner/conventions/appendix.convention.yaml +187 -0
  90. atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
  91. atdd/planner/conventions/component.convention.yaml +670 -0
  92. atdd/planner/conventions/criteria.convention.yaml +141 -0
  93. atdd/planner/conventions/feature.convention.yaml +371 -0
  94. atdd/planner/conventions/interface.convention.yaml +382 -0
  95. atdd/planner/conventions/steps.convention.yaml +141 -0
  96. atdd/planner/conventions/train.convention.yaml +552 -0
  97. atdd/planner/conventions/wagon.convention.yaml +275 -0
  98. atdd/planner/conventions/wmbt.convention.yaml +258 -0
  99. atdd/planner/schemas/acceptance.schema.json +336 -0
  100. atdd/planner/schemas/appendix.schema.json +78 -0
  101. atdd/planner/schemas/component.schema.json +114 -0
  102. atdd/planner/schemas/feature.schema.json +197 -0
  103. atdd/planner/schemas/train.schema.json +192 -0
  104. atdd/planner/schemas/wagon.schema.json +281 -0
  105. atdd/planner/schemas/wmbt.schema.json +59 -0
  106. atdd/planner/validators/__init__.py +0 -0
  107. atdd/planner/validators/conftest.py +5 -0
  108. atdd/planner/validators/test_draft_wagon_registry.py +374 -0
  109. atdd/planner/validators/test_plan_cross_refs.py +240 -0
  110. atdd/planner/validators/test_plan_uniqueness.py +224 -0
  111. atdd/planner/validators/test_plan_urn_resolution.py +268 -0
  112. atdd/planner/validators/test_plan_wagons.py +174 -0
  113. atdd/planner/validators/test_train_validation.py +514 -0
  114. atdd/planner/validators/test_wagon_urn_chain.py +648 -0
  115. atdd/planner/validators/test_wmbt_consistency.py +327 -0
  116. atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
  117. atdd/tester/__init__.py +1 -0
  118. atdd/tester/conventions/artifact.convention.yaml +257 -0
  119. atdd/tester/conventions/contract.convention.yaml +1009 -0
  120. atdd/tester/conventions/filename.convention.yaml +555 -0
  121. atdd/tester/conventions/migration.convention.yaml +509 -0
  122. atdd/tester/conventions/red.convention.yaml +797 -0
  123. atdd/tester/conventions/routing.convention.yaml +51 -0
  124. atdd/tester/conventions/telemetry.convention.yaml +458 -0
  125. atdd/tester/schemas/a11y.tmpl.json +17 -0
  126. atdd/tester/schemas/artifact.schema.json +189 -0
  127. atdd/tester/schemas/contract.schema.json +591 -0
  128. atdd/tester/schemas/contract.tmpl.json +95 -0
  129. atdd/tester/schemas/db.tmpl.json +20 -0
  130. atdd/tester/schemas/e2e.tmpl.json +17 -0
  131. atdd/tester/schemas/edge_function.tmpl.json +17 -0
  132. atdd/tester/schemas/event.tmpl.json +17 -0
  133. atdd/tester/schemas/http.tmpl.json +19 -0
  134. atdd/tester/schemas/job.tmpl.json +18 -0
  135. atdd/tester/schemas/load.tmpl.json +21 -0
  136. atdd/tester/schemas/metric.tmpl.json +19 -0
  137. atdd/tester/schemas/pack.schema.json +139 -0
  138. atdd/tester/schemas/realtime.tmpl.json +20 -0
  139. atdd/tester/schemas/rls.tmpl.json +18 -0
  140. atdd/tester/schemas/script.tmpl.json +16 -0
  141. atdd/tester/schemas/sec.tmpl.json +18 -0
  142. atdd/tester/schemas/storage.tmpl.json +18 -0
  143. atdd/tester/schemas/telemetry.schema.json +128 -0
  144. atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
  145. atdd/tester/schemas/test_filename.schema.json +194 -0
  146. atdd/tester/schemas/test_intent.schema.json +179 -0
  147. atdd/tester/schemas/unit.tmpl.json +18 -0
  148. atdd/tester/schemas/visual.tmpl.json +18 -0
  149. atdd/tester/schemas/ws.tmpl.json +17 -0
  150. atdd/tester/utils/__init__.py +0 -0
  151. atdd/tester/utils/filename.py +300 -0
  152. atdd/tester/validators/__init__.py +0 -0
  153. atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
  154. atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
  155. atdd/tester/validators/conftest.py +5 -0
  156. atdd/tester/validators/coverage_gap_report.py +321 -0
  157. atdd/tester/validators/fix_dual_ac_references.py +179 -0
  158. atdd/tester/validators/remove_duplicate_lines.py +93 -0
  159. atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
  160. atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
  161. atdd/tester/validators/test_artifact_naming_category.py +307 -0
  162. atdd/tester/validators/test_contract_schema_compliance.py +706 -0
  163. atdd/tester/validators/test_contracts_structure.py +200 -0
  164. atdd/tester/validators/test_coverage_adequacy.py +797 -0
  165. atdd/tester/validators/test_dual_ac_reference.py +225 -0
  166. atdd/tester/validators/test_fixture_validity.py +372 -0
  167. atdd/tester/validators/test_isolation.py +487 -0
  168. atdd/tester/validators/test_migration_coverage.py +204 -0
  169. atdd/tester/validators/test_migration_criteria.py +276 -0
  170. atdd/tester/validators/test_migration_generation.py +116 -0
  171. atdd/tester/validators/test_python_test_naming.py +410 -0
  172. atdd/tester/validators/test_red_layer_validation.py +95 -0
  173. atdd/tester/validators/test_red_python_layer_structure.py +87 -0
  174. atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
  175. atdd/tester/validators/test_telemetry_structure.py +634 -0
  176. atdd/tester/validators/test_typescript_test_naming.py +301 -0
  177. atdd/tester/validators/test_typescript_test_structure.py +84 -0
  178. atdd-0.1.0.dist-info/METADATA +191 -0
  179. atdd-0.1.0.dist-info/RECORD +183 -0
  180. atdd-0.1.0.dist-info/WHEEL +5 -0
  181. atdd-0.1.0.dist-info/entry_points.txt +2 -0
  182. atdd-0.1.0.dist-info/licenses/LICENSE +674 -0
  183. atdd-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,875 @@
1
+ #!/usr/bin/env python3
2
+ r"""
3
+ URN Construction Utility
4
+ ========================
5
+ Centralized URN generation for all entity types in the ATDD system.
6
+ All agents should use this utility to ensure consistent URN formatting.
7
+
8
+ URN Patterns:
9
+ - wagon: wagon:{kebab-case-name}
10
+ Example: wagon:resolve-dilemmas
11
+ Pattern: ^wagon:[a-z][a-z0-9-]*$
12
+
13
+ - feature: feature:{wagon}:{feature}
14
+ Example: feature:resolve-dilemmas:binary-choice
15
+ Pattern: ^feature:[a-z][a-z0-9-]*:[a-z][a-z0-9-]*$
16
+
17
+ - wmbt: wmbt:{wagon}:{STEP_CODE}{NNN}
18
+ Example: wmbt:resolve-dilemmas:E001
19
+ Pattern: ^wmbt:[a-z][a-z0-9-]*:[DLPCEMYRK][0-9]{3}$
20
+ Step Codes: D=define, L=locate, P=prepare, C=confirm, E=execute, M=monitor, Y=modify, R=resolve, K=conclude
21
+
22
+ - acceptance: acc:{wagon}:{wmbt_id}-{harness}-{NNN}[-{slug}]
23
+ Example: acc:authenticate-user:C004-E2E-019
24
+ acc:maintain-ux:C004-E2E-019-user-connection
25
+ Pattern: ^acc:[a-z][a-z0-9-]*:[DLPCEMYRK][0-9]{3}-(UNIT|HTTP|...)-[0-9]{3}(?:-[a-z0-9-]+)?$
26
+
27
+ - component: component:{wagon}:{feature}:{objectCamelCase}:{side}:{layer}
28
+ Example: component:resolve-dilemmas:binary-choice:OptionValidator:backend:domain
29
+ Pattern: ^component:[a-z][a-z0-9-]*:[a-z][a-z0-9-]*:[a-zA-Z0-9]+:(frontend|backend):(presentation|application|domain|integration)$
30
+ Side: frontend | backend
31
+ Layer: presentation | application | domain | integration
32
+
33
+ Usage:
34
+ from utils.graph import URNBuilder
35
+ # or
36
+ from utils.graph.urn import URNBuilder
37
+
38
+ # Build a wagon URN (verb-object format)
39
+ wagon_urn = URNBuilder.wagon("manage-users")
40
+
41
+ # Build a feature URN (verb-object format)
42
+ feature_urn = URNBuilder.feature("manage-users", "authenticate-user")
43
+
44
+ # Build a WMBT URN
45
+ wmbt_urn = URNBuilder.wmbt("manage-users", "E001")
46
+
47
+ # Build an acceptance URN
48
+ acc_urn = URNBuilder.acceptance("manage-users", "C004", "E2E", "019")
49
+ acc_urn_with_slug = URNBuilder.acceptance("manage-users", "C004", "E2E", "019", "user-login")
50
+
51
+ # Build a component URN
52
+ comp_urn = URNBuilder.component("manage-users", "authenticate-user", "LoginForm", "frontend", "presentation")
53
+
54
+ # Build a test URN
55
+ test_urn = URNBuilder.test("manage-users", "tc-login-success", feature_id="authenticate-user")
56
+ """
57
+
58
+ import re
59
+ import sys
60
+ from pathlib import Path
61
+ from typing import Optional, Literal
62
+
63
+ # No logger needed - removed _bootstrap dependency
64
+
65
+ class URNBuilder:
66
+ """Centralized URN builder for all entity types."""
67
+
68
+ STEP_LEGEND = {
69
+ "D": "define",
70
+ "L": "locate",
71
+ "P": "prepare",
72
+ "C": "confirm",
73
+ "E": "execute",
74
+ "M": "monitor",
75
+ "Y": "modify",
76
+ "R": "resolve",
77
+ "K": "conclude",
78
+ }
79
+ STEP_NAMES = STEP_LEGEND
80
+ STEP_CODE_LEGEND = STEP_LEGEND
81
+ STEP_NAME_TO_CODE = {name: code for code, name in STEP_LEGEND.items()}
82
+
83
+ # Harness code mapping (authoritative)
84
+ HARNESS_CODES = {
85
+ 'unit': 'UNIT',
86
+ 'http': 'HTTP',
87
+ 'event': 'EVENT',
88
+ 'ws': 'WS',
89
+ 'e2e': 'E2E',
90
+ 'a11y': 'A11Y',
91
+ 'visual': 'VIS',
92
+ 'metric': 'METRIC',
93
+ 'job': 'JOB',
94
+ 'db': 'DB',
95
+ 'sec': 'SEC',
96
+ 'load': 'LOAD',
97
+ 'script': 'SCRIPT',
98
+ 'widget': 'WIDGET',
99
+ 'golden': 'GOLDEN',
100
+ 'bloc': 'BLOC',
101
+ 'integration': 'INTEGRATION',
102
+ 'rls': 'RLS',
103
+ 'edge_function': 'EDGE',
104
+ 'realtime': 'REALTIME',
105
+ 'storage': 'STORAGE'
106
+ }
107
+
108
+ _MANIFEST_STATE = {}
109
+
110
+ # Pattern validators
111
+ PATTERNS = {
112
+ # Identities
113
+ 'wagon': r'^wagon:[a-z][a-z0-9-]*$',
114
+ 'feature': r'^feature:[a-z][a-z0-9-]*:[a-z][a-z0-9-]*$',
115
+ 'component': r'^component:[a-z][a-z0-9-]*:[a-z][a-z0-9-]*:[a-zA-Z0-9]+:(frontend|backend|fe|be):(presentation|application|domain|integration|controller|usecase|repository)$',
116
+
117
+ # Artifacts
118
+ 'plan': r'^plan:[a-z0-9]+(-[a-z0-9]+)*(\.[a-z0-9]+(-[a-z0-9]+)*)?(\.[a-zA-Z0-9]+\.(frontend|backend|fe|be)\.(presentation|application|domain|integration|controller|usecase|repository))?$',
119
+ 'test': r'^test:[a-z0-9]+(-[a-z0-9]+)*(\.[a-z0-9]+(-[a-z0-9]+)*)?(\.[a-zA-Z0-9]+\.(frontend|backend|fe|be)\.(presentation|application|domain|integration|controller|usecase|repository))?\.[a-z0-9-]+$',
120
+ 'contract': r'^contract:[a-z0-9]+(-[a-z0-9]+)*(\.[a-z0-9]+(-[a-z0-9]+)*)?(\.[a-zA-Z0-9]+\.(frontend|backend|fe|be)\.(presentation|application|domain|integration|controller|usecase|repository))?$',
121
+ 'telemetry': r'^telemetry:[a-z0-9]+(-[a-z0-9]+)*(\.[a-z0-9]+(-[a-z0-9]+)*)?(\.[a-zA-Z0-9]+\.(frontend|backend|fe|be)\.(presentation|application|domain|integration|controller|usecase|repository))?\.[a-z0-9-]+$',
122
+
123
+ # ATDD Specific
124
+ 'wmbt': r'^wmbt:[a-z][a-z0-9-]*:[DLPCEMYRK][0-9]{3}$',
125
+ 'acc': r'^acc:[a-z][a-z0-9-]*:[DLPCEMYRK][0-9]{3}-(UNIT|HTTP|EVENT|WS|E2E|A11Y|VIS|METRIC|JOB|DB|SEC|LOAD|SCRIPT|WIDGET|GOLDEN|BLOC|INTEGRATION|RLS|EDGE|REALTIME|STORAGE)-[0-9]{3}(?:-[a-z0-9-]+)?$',
126
+
127
+ # Resources
128
+ 'endpoint': r'^endpoint:[a-z0-9-]+\.(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\.[a-z0-9-/]+$',
129
+ 'topic': r'^topic:[a-z0-9-]+$',
130
+ 'table': r'^table:[a-z0-9_]+$',
131
+ 'team': r'^team:[a-z0-9-]+$'
132
+ }
133
+
134
+ @classmethod
135
+ def validate_urn(cls, urn: str, entity_type: str) -> bool:
136
+ """Validate that a URN matches the expected pattern."""
137
+ pattern = cls.PATTERNS.get(entity_type)
138
+ if not pattern:
139
+ raise ValueError(f"Unknown entity type: {entity_type}")
140
+ return bool(re.match(pattern, urn))
141
+
142
+ @classmethod
143
+ def wagon(cls, wagon_id: str) -> str:
144
+ """
145
+ Build a wagon URN.
146
+
147
+ Args:
148
+ wagon_id: The wagon identifier (lowercase, alphanumeric with hyphens)
149
+
150
+ Returns:
151
+ URN in format: wagon:[wagon_id]
152
+
153
+ Example:
154
+ URNBuilder.wagon("manage-users") -> "wagon:manage-users"
155
+ """
156
+ # Normalize the wagon ID
157
+ wagon_id = cls._normalize_id(wagon_id)
158
+
159
+ # Validate format
160
+ if not re.match(r'^[a-z][a-z0-9-]*$', wagon_id):
161
+ raise ValueError(f"Invalid wagon ID format: {wagon_id}. Must start with lowercase letter, contain only lowercase alphanumeric and hyphens.")
162
+
163
+ urn = f"wagon:{wagon_id}"
164
+
165
+ if not cls.validate_urn(urn, 'wagon'):
166
+ raise ValueError(f"Generated invalid wagon URN: {urn}")
167
+
168
+ return urn
169
+
170
+ @classmethod
171
+ def feature(cls, wagon_id: str, feature_id: str) -> str:
172
+ """
173
+ Build a feature URN.
174
+
175
+ Args:
176
+ wagon_id: The parent wagon identifier
177
+ feature_id: The feature identifier
178
+
179
+ Returns:
180
+ URN in format: feature:[wagon_id]:[feature_id]
181
+
182
+ Example:
183
+ URNBuilder.feature("manage-users", "authenticate-user") -> "feature:manage-users:authenticate-user"
184
+ """
185
+ # Normalize IDs
186
+ wagon_id = cls._normalize_id(wagon_id)
187
+ feature_id = cls._normalize_id(feature_id)
188
+
189
+ # Validate format
190
+ if not re.match(r'^[a-z][a-z0-9-]*$', wagon_id):
191
+ raise ValueError(f"Invalid wagon ID for feature: {wagon_id}")
192
+ if not re.match(r'^[a-z][a-z0-9-]*$', feature_id):
193
+ raise ValueError(f"Invalid feature ID: {feature_id}")
194
+
195
+ urn = f"feature:{wagon_id}:{feature_id}"
196
+
197
+ if not cls.validate_urn(urn, 'feature'):
198
+ raise ValueError(f"Generated invalid feature URN: {urn}")
199
+
200
+ return urn
201
+
202
+ @classmethod
203
+ def wmbt(cls, wagon_id: str, sequence: str) -> str:
204
+ """
205
+ Build a WMBT URN.
206
+
207
+ Args:
208
+ wagon_id: The parent wagon identifier
209
+ sequence: Step-coded identifier (e.g., "E001")
210
+
211
+ Returns:
212
+ URN in format: wmbt:[wagon_id]:[sequence]
213
+
214
+ Example:
215
+ URNBuilder.wmbt("user-auth", "E001") -> "wmbt:user-auth:E001"
216
+ """
217
+ # Normalize wagon ID
218
+ wagon_id = cls._normalize_id(wagon_id)
219
+
220
+ step_coded_id = cls._normalize_wmbt_id(sequence)
221
+
222
+ # Validate wagon ID format
223
+ if not re.match(r'^[a-z][a-z0-9-]*$', wagon_id):
224
+ raise ValueError(f"Invalid wagon ID for WMBT: {wagon_id}")
225
+
226
+ urn = f"wmbt:{wagon_id}:{step_coded_id}"
227
+
228
+ if not cls.validate_urn(urn, 'wmbt'):
229
+ raise ValueError(f"Generated invalid WMBT URN: {urn}")
230
+
231
+ return urn
232
+
233
+ @classmethod
234
+ def step_from_id(cls, wmbt_id: str) -> str:
235
+ """Derive the canonical step name from a step-coded WMBT id."""
236
+ if not isinstance(wmbt_id, str):
237
+ raise TypeError("wmbt_id must be a string")
238
+
239
+ match = re.fullmatch(r'^[DLPCEMYRK][0-9]{3}$', wmbt_id.strip())
240
+ if not match:
241
+ raise ValueError(f"Invalid WMBT id format: {wmbt_id}")
242
+
243
+ return cls.STEP_LEGEND[wmbt_id[0]]
244
+
245
+ @classmethod
246
+ def next_wmbt_id(cls, manifest: dict, step: str) -> str:
247
+ """Return the next step-coded id for a given manifest and step."""
248
+ if manifest is None:
249
+ manifest = {}
250
+
251
+ step_code = cls._normalize_step(step)
252
+ current_wagon = manifest.get('wagon')
253
+
254
+ state = cls._MANIFEST_STATE.get(id(manifest))
255
+ if state is None or state.get('wagon') != current_wagon:
256
+ state = {'wagon': current_wagon, 'counters': {}}
257
+ cls._MANIFEST_STATE[id(manifest)] = state
258
+
259
+ counters = state['counters']
260
+ current_counter = counters.get(step_code)
261
+
262
+ if current_counter is None:
263
+ existing = manifest.get('wmbt') or {}
264
+ if not isinstance(existing, dict):
265
+ existing = {}
266
+
267
+ wagon_slug = current_wagon or ""
268
+ wagon_token = wagon_slug.split('-')[0] if wagon_slug else ""
269
+ produce_entries = manifest.get('produce') or []
270
+ if produce_entries and wagon_token:
271
+ if all(wagon_slug not in str(entry) and wagon_token not in str(entry) for entry in produce_entries):
272
+ existing = {}
273
+
274
+ pattern = re.compile(rf'^{step_code}(\d{{3}})$')
275
+ max_index = 0
276
+ for key in existing.keys():
277
+ if not isinstance(key, str):
278
+ continue
279
+ match = pattern.match(key)
280
+ if match:
281
+ max_index = max(max_index, int(match.group(1)))
282
+
283
+ current_counter = max_index
284
+
285
+ if current_counter >= 999:
286
+ raise ValueError(f"No remaining ids for step {step}")
287
+
288
+ next_index = current_counter + 1
289
+ counters[step_code] = next_index
290
+
291
+ return f"{step_code}{next_index:03d}"
292
+
293
+ @classmethod
294
+ def _normalize_step(cls, step: str) -> str:
295
+ if not isinstance(step, str):
296
+ raise TypeError("step must be a string")
297
+
298
+ cleaned = step.strip()
299
+ if not cleaned:
300
+ raise ValueError("step cannot be empty")
301
+
302
+ upper = cleaned.upper()
303
+ if upper in cls.STEP_LEGEND:
304
+ return upper
305
+
306
+ lower = cleaned.lower()
307
+ code = cls.STEP_NAME_TO_CODE.get(lower)
308
+ if code:
309
+ return code
310
+
311
+ raise ValueError(f"Unknown step: {step}")
312
+
313
+ @classmethod
314
+ def _normalize_wmbt_id(cls, wmbt_id) -> str:
315
+ if isinstance(wmbt_id, str):
316
+ candidate = wmbt_id.strip().upper()
317
+ if re.fullmatch(r'^[DLPCEMYRK][0-9]{3}$', candidate):
318
+ return candidate
319
+ raise ValueError("WMBT id must match pattern [DLPCEMYRK][0-9]{3}")
320
+
321
+ raise TypeError("WMBT id must be provided as a step-coded string")
322
+
323
+ @classmethod
324
+ def _normalize_acceptance_sequence(cls, sequence) -> str:
325
+ """Accept numeric or step-coded sequence values for acceptance URNs."""
326
+ if isinstance(sequence, int):
327
+ if sequence <= 0 or sequence > 999:
328
+ raise ValueError("WMBT sequence must be between 1 and 999")
329
+ return f"{sequence:03d}"
330
+
331
+ if isinstance(sequence, str):
332
+ cleaned = sequence.strip()
333
+ if not cleaned:
334
+ raise ValueError("WMBT sequence cannot be empty")
335
+
336
+ upper = cleaned.upper()
337
+ if re.fullmatch(r'^[DLPCEMYRK][0-9]{3}$', upper):
338
+ return upper
339
+
340
+ if re.fullmatch(r'^\d{1,3}$', cleaned):
341
+ value = int(cleaned)
342
+ if value <= 0 or value > 999:
343
+ raise ValueError("WMBT sequence must be between 1 and 999")
344
+ return f"{value:03d}"
345
+
346
+ raise ValueError("WMBT sequence must be a step-coded id or 1-3 digit number")
347
+
348
+ raise TypeError("WMBT sequence must be an int or string")
349
+
350
+ @classmethod
351
+ def acceptance(cls, wagon_id: str, wmbt_id: str, harness_code: str, seq, slug: Optional[str] = None) -> str:
352
+ """
353
+ Build an acceptance URN (refactored format).
354
+
355
+ Args:
356
+ wagon_id: The parent wagon identifier
357
+ wmbt_id: The WMBT ID (step code + seq, e.g., "C004", "E001")
358
+ harness_code: The harness code (UPPERCASE, e.g., "E2E", "UNIT", "HTTP")
359
+ seq: The per-harness sequence number (int or string, 001-999)
360
+ slug: Optional kebab-case descriptor for readability
361
+
362
+ Returns:
363
+ URN in format: acc:{wagon}:{wmbt_id}-{harness}-{NNN}[-{slug}]
364
+
365
+ Examples:
366
+ URNBuilder.acceptance("authenticate-user", "C004", "E2E", "019")
367
+ -> "acc:authenticate-user:C004-E2E-019"
368
+
369
+ URNBuilder.acceptance("maintain-ux", "C004", "E2E", "019", "user-connection")
370
+ -> "acc:maintain-ux:C004-E2E-019-user-connection"
371
+ """
372
+ # Normalize wagon ID
373
+ wagon_id = cls._normalize_id(wagon_id)
374
+
375
+ # Validate and normalize WMBT ID
376
+ wmbt_id = cls._normalize_wmbt_id(wmbt_id)
377
+
378
+ # Validate harness code
379
+ harness_code = harness_code.upper()
380
+ valid_harnesses = set(cls.HARNESS_CODES.values())
381
+ if harness_code not in valid_harnesses:
382
+ raise ValueError(
383
+ f"Invalid harness code: {harness_code}. "
384
+ f"Must be one of: {', '.join(sorted(valid_harnesses))}"
385
+ )
386
+
387
+ # Normalize and pad sequence
388
+ if isinstance(seq, int):
389
+ if seq <= 0 or seq > 999:
390
+ raise ValueError("Sequence must be between 1 and 999")
391
+ seq_str = f"{seq:03d}"
392
+ elif isinstance(seq, str):
393
+ seq_clean = seq.strip()
394
+ if not re.match(r'^\d{1,3}$', seq_clean):
395
+ raise ValueError("Sequence must be 1-3 digit number")
396
+ seq_int = int(seq_clean)
397
+ if seq_int <= 0 or seq_int > 999:
398
+ raise ValueError("Sequence must be between 1 and 999")
399
+ seq_str = f"{seq_int:03d}"
400
+ else:
401
+ raise TypeError("Sequence must be int or string")
402
+
403
+ # Build URN
404
+ urn = f"acc:{wagon_id}:{wmbt_id}-{harness_code}-{seq_str}"
405
+
406
+ # Add optional slug
407
+ if slug:
408
+ slug_normalized = cls._normalize_id(slug)
409
+ urn += f"-{slug_normalized}"
410
+
411
+ # Validate final URN
412
+ if not cls.validate_urn(urn, 'acc'):
413
+ raise ValueError(f"Generated invalid acceptance URN: {urn}")
414
+
415
+ return urn
416
+
417
+ @classmethod
418
+ def component(cls,
419
+ wagon_id: str,
420
+ feature_id: str,
421
+ component_name: str,
422
+ side: Literal['frontend', 'backend'],
423
+ layer: Literal['presentation', 'application', 'domain', 'integration']) -> str:
424
+ """
425
+ Build a component URN.
426
+
427
+ Args:
428
+ wagon_id: The parent wagon identifier
429
+ feature_id: The parent feature identifier
430
+ component_name: The component name (PascalCase or camelCase)
431
+ side: Either 'frontend' or 'backend'
432
+ layer: The architectural layer
433
+
434
+ Returns:
435
+ URN in format: component:[wagon_id]:[feature_id]:[component_name]:[side]:[layer]
436
+
437
+ Example:
438
+ URNBuilder.component("user-mgmt", "auth", "LoginForm", "frontend", "presentation")
439
+ -> "component:user-mgmt:auth:LoginForm:frontend:presentation"
440
+ """
441
+ # Normalize IDs (but preserve component name case)
442
+ wagon_id = cls._normalize_id(wagon_id)
443
+ feature_id = cls._normalize_id(feature_id)
444
+
445
+ # Validate formats
446
+ if not re.match(r'^[a-z][a-z0-9-]*$', wagon_id):
447
+ raise ValueError(f"Invalid wagon ID for component: {wagon_id}")
448
+ if not re.match(r'^[a-z][a-z0-9-]*$', feature_id):
449
+ raise ValueError(f"Invalid feature ID for component: {feature_id}")
450
+ if not re.match(r'^[a-zA-Z0-9]+$', component_name):
451
+ raise ValueError(f"Invalid component name: {component_name}. Must be alphanumeric.")
452
+ if side not in ['frontend', 'backend']:
453
+ raise ValueError(f"Invalid side: {side}. Must be 'frontend' or 'backend'.")
454
+ if layer not in ['presentation', 'application', 'domain', 'integration']:
455
+ raise ValueError(f"Invalid layer: {layer}. Must be one of: presentation, application, domain, integration.")
456
+
457
+ urn = f"component:{wagon_id}:{feature_id}:{component_name}:{side}:{layer}"
458
+
459
+ if not cls.validate_urn(urn, 'component'):
460
+ raise ValueError(f"Generated invalid component URN: {urn}")
461
+
462
+ return urn
463
+
464
+ @classmethod
465
+ def plan(cls,
466
+ wagon_id: str,
467
+ feature_id: Optional[str] = None,
468
+ component_name: Optional[str] = None,
469
+ side: Optional[Literal['frontend', 'backend', 'fe', 'be']] = None,
470
+ layer: Optional[Literal['presentation', 'application', 'domain', 'integration', 'controller', 'usecase', 'repository']] = None) -> str:
471
+ """
472
+ Build a plan URN.
473
+
474
+ Args:
475
+ wagon_id: The wagon identifier
476
+ feature_id: Optional feature identifier
477
+ component_name: Optional component name
478
+ side: Optional component side (requires component_name)
479
+ layer: Optional architectural layer (requires component_name and side)
480
+
481
+ Returns:
482
+ URN in format: plan:[wagon][.[feature][.[component].[side].[layer]]]
483
+
484
+ Examples:
485
+ URNBuilder.plan("user-mgmt")
486
+ -> "plan:user-mgmt"
487
+
488
+ URNBuilder.plan("user-mgmt", feature_id="auth")
489
+ -> "plan:user-mgmt.auth"
490
+
491
+ URNBuilder.plan("user-mgmt", feature_id="auth",
492
+ component_name="LoginForm", side="fe", layer="presentation")
493
+ -> "plan:user-mgmt.auth.LoginForm.fe.presentation"
494
+ """
495
+ # Normalize IDs
496
+ wagon_id = cls._normalize_id(wagon_id)
497
+
498
+ # Build URN progressively
499
+ urn = f"plan:{wagon_id}"
500
+
501
+ if feature_id:
502
+ feature_id = cls._normalize_id(feature_id)
503
+ urn += f".{feature_id}"
504
+
505
+ if component_name:
506
+ if not side or not layer:
507
+ raise ValueError("Component requires both side and layer")
508
+ urn += f".{component_name}.{side}.{layer}"
509
+ elif component_name:
510
+ raise ValueError("Cannot specify component without feature")
511
+
512
+ if not cls.validate_urn(urn, 'plan'):
513
+ raise ValueError(f"Generated invalid plan URN: {urn}")
514
+
515
+ return urn
516
+
517
+ @classmethod
518
+ def contract(cls,
519
+ wagon_id: str,
520
+ feature_id: Optional[str] = None,
521
+ component_name: Optional[str] = None,
522
+ side: Optional[Literal['frontend', 'backend', 'fe', 'be']] = None,
523
+ layer: Optional[Literal['presentation', 'application', 'domain', 'integration', 'controller', 'usecase', 'repository']] = None) -> str:
524
+ """
525
+ Build a contract URN.
526
+
527
+ Args:
528
+ wagon_id: The wagon identifier
529
+ feature_id: Optional feature identifier
530
+ component_name: Optional component name
531
+ side: Optional component side (requires component_name)
532
+ layer: Optional architectural layer (requires component_name and side)
533
+
534
+ Returns:
535
+ URN in format: contract:[wagon][.[feature][.[component].[side].[layer]]]
536
+
537
+ Examples:
538
+ URNBuilder.contract("user-mgmt")
539
+ -> "contract:user-mgmt"
540
+
541
+ URNBuilder.contract("user-mgmt", feature_id="auth")
542
+ -> "contract:user-mgmt.auth"
543
+
544
+ URNBuilder.contract("user-mgmt", feature_id="auth",
545
+ component_name="UserAPI", side="be", layer="controller")
546
+ -> "contract:user-mgmt.auth.UserAPI.be.controller"
547
+ """
548
+ # Normalize IDs
549
+ wagon_id = cls._normalize_id(wagon_id)
550
+
551
+ # Build URN progressively
552
+ urn = f"contract:{wagon_id}"
553
+
554
+ if feature_id:
555
+ feature_id = cls._normalize_id(feature_id)
556
+ urn += f".{feature_id}"
557
+
558
+ if component_name:
559
+ if not side or not layer:
560
+ raise ValueError("Component requires both side and layer")
561
+ urn += f".{component_name}.{side}.{layer}"
562
+ elif component_name:
563
+ raise ValueError("Cannot specify component without feature")
564
+
565
+ if not cls.validate_urn(urn, 'contract'):
566
+ raise ValueError(f"Generated invalid contract URN: {urn}")
567
+
568
+ return urn
569
+
570
+ @classmethod
571
+ def telemetry(cls,
572
+ wagon_id: str,
573
+ signal: str,
574
+ feature_id: Optional[str] = None,
575
+ component_name: Optional[str] = None,
576
+ side: Optional[Literal['frontend', 'backend', 'fe', 'be']] = None,
577
+ layer: Optional[Literal['presentation', 'application', 'domain', 'integration', 'controller', 'usecase', 'repository']] = None) -> str:
578
+ """
579
+ Build a telemetry URN.
580
+
581
+ Args:
582
+ wagon_id: The wagon identifier
583
+ signal: The signal identifier (e.g., "metric-response-time", "event-click")
584
+ feature_id: Optional feature identifier
585
+ component_name: Optional component name
586
+ side: Optional component side (requires component_name)
587
+ layer: Optional architectural layer (requires component_name and side)
588
+
589
+ Returns:
590
+ URN in format: telemetry:[wagon][.[feature][.[component].[side].[layer]]].[signal]
591
+
592
+ Examples:
593
+ URNBuilder.telemetry("user-mgmt", "metric-response-time")
594
+ -> "telemetry:user-mgmt.metric-response-time"
595
+
596
+ URNBuilder.telemetry("user-mgmt", "event-login", feature_id="auth")
597
+ -> "telemetry:user-mgmt.auth.event-login"
598
+
599
+ URNBuilder.telemetry("user-mgmt", "event-click", feature_id="auth",
600
+ component_name="LoginForm", side="fe", layer="presentation")
601
+ -> "telemetry:user-mgmt.auth.LoginForm.fe.presentation.event-click"
602
+ """
603
+ # Normalize IDs
604
+ wagon_id = cls._normalize_id(wagon_id)
605
+ signal = cls._normalize_id(signal)
606
+
607
+ # Build URN progressively
608
+ urn = f"telemetry:{wagon_id}"
609
+
610
+ if feature_id:
611
+ feature_id = cls._normalize_id(feature_id)
612
+ urn += f".{feature_id}"
613
+
614
+ if component_name:
615
+ if not side or not layer:
616
+ raise ValueError("Component requires both side and layer")
617
+ urn += f".{component_name}.{side}.{layer}"
618
+ elif component_name:
619
+ raise ValueError("Cannot specify component without feature")
620
+
621
+ urn += f".{signal}"
622
+
623
+ if not cls.validate_urn(urn, 'telemetry'):
624
+ raise ValueError(f"Generated invalid telemetry URN: {urn}")
625
+
626
+ return urn
627
+
628
+ @classmethod
629
+ def test(cls,
630
+ wagon_id: str,
631
+ test_case: str,
632
+ feature_id: Optional[str] = None,
633
+ component_name: Optional[str] = None,
634
+ side: Optional[Literal['frontend', 'backend', 'fe', 'be']] = None,
635
+ layer: Optional[Literal['presentation', 'application', 'domain', 'integration', 'controller', 'usecase', 'repository']] = None) -> str:
636
+ """
637
+ Build a test URN.
638
+
639
+ Args:
640
+ wagon_id: The wagon identifier
641
+ test_case: The test case identifier (e.g., "tc-login-success")
642
+ feature_id: Optional feature identifier
643
+ component_name: Optional component name
644
+ side: Optional component side (requires component_name)
645
+ layer: Optional architectural layer (requires component_name and side)
646
+
647
+ Returns:
648
+ URN in format: test:[wagon][.[feature][.[component].[side].[layer]]].[test_case]
649
+
650
+ Examples:
651
+ URNBuilder.test("user-mgmt", "tc-basic-flow")
652
+ -> "test:user-mgmt.tc-basic-flow"
653
+
654
+ URNBuilder.test("user-mgmt", "tc-login", feature_id="auth")
655
+ -> "test:user-mgmt.auth.tc-login"
656
+
657
+ URNBuilder.test("user-mgmt", "tc-render", feature_id="auth",
658
+ component_name="LoginForm", side="fe", layer="presentation")
659
+ -> "test:user-mgmt.auth.LoginForm.fe.presentation.tc-render"
660
+ """
661
+ # Normalize IDs
662
+ wagon_id = cls._normalize_id(wagon_id)
663
+ test_case = cls._normalize_id(test_case)
664
+
665
+ # Build URN progressively
666
+ urn = f"test:{wagon_id}"
667
+
668
+ if feature_id:
669
+ feature_id = cls._normalize_id(feature_id)
670
+ urn += f".{feature_id}"
671
+
672
+ if component_name:
673
+ if not side or not layer:
674
+ raise ValueError("Component requires both side and layer")
675
+ urn += f".{component_name}.{side}.{layer}"
676
+
677
+ urn += f".{test_case}"
678
+
679
+ if not cls.validate_urn(urn, 'test'):
680
+ raise ValueError(f"Generated invalid test URN: {urn}")
681
+
682
+ return urn
683
+
684
+ @classmethod
685
+ def parse_urn(cls, urn: str) -> dict:
686
+ """
687
+ Parse a URN into its components.
688
+
689
+ Args:
690
+ urn: The URN to parse
691
+
692
+ Returns:
693
+ Dictionary with URN components
694
+
695
+ Example:
696
+ URNBuilder.parse_urn("acc:user-auth.001.AC-EXEC-201")
697
+ -> {
698
+ 'type': 'acceptance',
699
+ 'wagon_id': 'user-auth',
700
+ 'wmbt_sequence': '001',
701
+ 'acceptance_id': 'AC-EXEC-201'
702
+ }
703
+ """
704
+ # Determine the type
705
+ if urn.startswith('wagon:'):
706
+ return {
707
+ 'type': 'wagon',
708
+ 'wagon_id': urn.replace('wagon:', '')
709
+ }
710
+ elif urn.startswith('feature:'):
711
+ parts = urn.replace('feature:', '').split(':')
712
+ return {
713
+ 'type': 'feature',
714
+ 'wagon_id': parts[0],
715
+ 'feature_id': parts[1] if len(parts) > 1 else None
716
+ }
717
+ elif urn.startswith('wmbt:'):
718
+ parts = urn.replace('wmbt:', '').split(':')
719
+ return {
720
+ 'type': 'wmbt',
721
+ 'wagon_id': parts[0],
722
+ 'sequence': parts[1] if len(parts) > 1 else None
723
+ }
724
+ elif urn.startswith('acc:'):
725
+ main_part = urn.replace('acc:', '')
726
+ parts = main_part.split(':')
727
+ # Format: wagon_id:wmbt_id-harness-seq[-slug]
728
+ result = {
729
+ 'type': 'acceptance',
730
+ 'wagon_id': parts[0] if len(parts) > 0 else None,
731
+ }
732
+
733
+ # Parse facets: wmbt_id-harness-seq[-slug]
734
+ if len(parts) > 1:
735
+ facets = parts[1].split('-')
736
+ if len(facets) >= 3:
737
+ result['wmbt_id'] = facets[0] # e.g., C004
738
+ result['harness'] = facets[1] # e.g., E2E
739
+ result['sequence'] = facets[2] # e.g., 019
740
+ # Optional slug (remaining parts joined with hyphens)
741
+ if len(facets) > 3:
742
+ result['slug'] = '-'.join(facets[3:])
743
+
744
+ return result
745
+ elif urn.startswith('test:'):
746
+ main_part = urn.replace('test:', '')
747
+ parts = main_part.split('.')
748
+ # Last part is always the test case
749
+ test_case = parts[-1] if parts else None
750
+ # Rest follows wagon.feature.component.side.layer pattern
751
+ result = {
752
+ 'type': 'test',
753
+ 'wagon_id': parts[0] if len(parts) > 0 else None,
754
+ 'test_case': test_case
755
+ }
756
+ if len(parts) > 2:
757
+ result['feature_id'] = parts[1]
758
+ if len(parts) > 5: # Has component
759
+ result['component_name'] = parts[2]
760
+ result['side'] = parts[3]
761
+ result['layer'] = parts[4]
762
+ return result
763
+ elif urn.startswith('component:'):
764
+ parts = urn.replace('component:', '').split(':')
765
+ return {
766
+ 'type': 'component',
767
+ 'wagon_id': parts[0] if len(parts) > 0 else None,
768
+ 'feature_id': parts[1] if len(parts) > 1 else None,
769
+ 'component_name': parts[2] if len(parts) > 2 else None,
770
+ 'side': parts[3] if len(parts) > 3 else None,
771
+ 'layer': parts[4] if len(parts) > 4 else None
772
+ }
773
+ else:
774
+ raise ValueError(f"Unknown URN type: {urn}")
775
+
776
+ @staticmethod
777
+ def _normalize_id(identifier: str) -> str:
778
+ """Normalize an identifier to lowercase with hyphens."""
779
+ # Convert to lowercase
780
+ normalized = identifier.lower()
781
+ # Replace underscores with hyphens
782
+ normalized = normalized.replace('_', '-')
783
+ # Remove any spaces
784
+ normalized = normalized.replace(' ', '-')
785
+ # Collapse multiple hyphens
786
+ normalized = re.sub(r'-+', '-', normalized)
787
+ # Remove leading/trailing hyphens
788
+ normalized = normalized.strip('-')
789
+ return normalized
790
+
791
+
792
+ def main() -> int:
793
+ """CLI interface for URN generation."""
794
+ import argparse
795
+ import json
796
+
797
+ parser = argparse.ArgumentParser(description='Generate URNs for ATDD entities')
798
+ subparsers = parser.add_subparsers(dest='entity', help='Entity type')
799
+
800
+ wagon_parser = subparsers.add_parser('wagon', help='Generate wagon URN')
801
+ wagon_parser.add_argument('wagon_id', help='Wagon identifier')
802
+
803
+ feature_parser = subparsers.add_parser('feature', help='Generate feature URN')
804
+ feature_parser.add_argument('wagon_id', help='Parent wagon identifier')
805
+ feature_parser.add_argument('feature_id', help='Feature identifier')
806
+
807
+ wmbt_parser = subparsers.add_parser('wmbt', help='Generate WMBT URN')
808
+ wmbt_parser.add_argument('wagon_id', help='Parent wagon identifier')
809
+ wmbt_parser.add_argument('sequence', help='Three-digit sequence (e.g., 001)')
810
+
811
+ acc_parser = subparsers.add_parser('acceptance', help='Generate acceptance URN')
812
+ acc_parser.add_argument('wagon_id', help='Parent wagon identifier')
813
+ acc_parser.add_argument('wmbt_sequence', help='WMBT sequence number')
814
+ acc_parser.add_argument('acceptance_id', help='Acceptance ID (e.g., AC-EXEC-201)')
815
+
816
+ comp_parser = subparsers.add_parser('component', help='Generate component URN')
817
+ comp_parser.add_argument('wagon_id', help='Parent wagon identifier')
818
+ comp_parser.add_argument('feature_id', help='Parent feature identifier')
819
+ comp_parser.add_argument('component_name', help='Component name')
820
+ comp_parser.add_argument('side', choices=['frontend', 'backend'], help='Component side')
821
+ comp_parser.add_argument('layer', choices=['presentation', 'application', 'domain', 'integration'], help='Architectural layer')
822
+
823
+ parse_parser = subparsers.add_parser('parse', help='Parse a URN')
824
+ parse_parser.add_argument('urn', help='URN to parse')
825
+
826
+ validate_parser = subparsers.add_parser('validate', help='Validate a URN')
827
+ validate_parser.add_argument('urn', help='URN to validate')
828
+ validate_parser.add_argument('entity_type', choices=['wagon', 'feature', 'wmbt', 'acceptance', 'component'], help='Expected entity type')
829
+
830
+ args = parser.parse_args()
831
+
832
+ if not args.entity:
833
+ parser.print_help()
834
+ return 1
835
+
836
+ exit_code = 0
837
+
838
+ try:
839
+ if args.entity == 'wagon':
840
+ urn = URNBuilder.wagon(args.wagon_id)
841
+ print(urn)
842
+ elif args.entity == 'feature':
843
+ urn = URNBuilder.feature(args.wagon_id, args.feature_id)
844
+ print(urn)
845
+ elif args.entity == 'wmbt':
846
+ urn = URNBuilder.wmbt(args.wagon_id, args.sequence)
847
+ print(urn)
848
+ elif args.entity == 'acceptance':
849
+ urn = URNBuilder.acceptance(args.wagon_id, args.wmbt_sequence, args.acceptance_id)
850
+ print(urn)
851
+ elif args.entity == 'component':
852
+ urn = URNBuilder.component(args.wagon_id, args.feature_id, args.component_name, args.side, args.layer)
853
+ print(urn)
854
+ elif args.entity == 'parse':
855
+ result = URNBuilder.parse_urn(args.urn)
856
+ print(json.dumps(result, indent=2))
857
+ elif args.entity == 'validate':
858
+ is_valid = URNBuilder.validate_urn(args.urn, args.entity_type)
859
+ if is_valid:
860
+ print(f"✓ Valid {args.entity_type} URN")
861
+ else:
862
+ print(f"✗ Invalid {args.entity_type} URN")
863
+ exit_code = 1
864
+ else:
865
+ print(f"Unsupported entity: {args.entity}")
866
+ return 1
867
+ except ValueError as exc:
868
+ print(f"Error: {exc}", file=sys.stderr)
869
+ return 1
870
+
871
+ return exit_code
872
+
873
+
874
+ if __name__ == '__main__':
875
+ main()