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.
- atdd/__init__.py +0 -0
- atdd/cli.py +404 -0
- atdd/coach/__init__.py +0 -0
- atdd/coach/commands/__init__.py +0 -0
- atdd/coach/commands/add_persistence_metadata.py +215 -0
- atdd/coach/commands/analyze_migrations.py +188 -0
- atdd/coach/commands/consumers.py +720 -0
- atdd/coach/commands/infer_governance_status.py +149 -0
- atdd/coach/commands/initializer.py +177 -0
- atdd/coach/commands/interface.py +1078 -0
- atdd/coach/commands/inventory.py +565 -0
- atdd/coach/commands/migration.py +240 -0
- atdd/coach/commands/registry.py +1560 -0
- atdd/coach/commands/session.py +430 -0
- atdd/coach/commands/sync.py +405 -0
- atdd/coach/commands/test_interface.py +399 -0
- atdd/coach/commands/test_runner.py +141 -0
- atdd/coach/commands/tests/__init__.py +1 -0
- atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
- atdd/coach/commands/traceability.py +4264 -0
- atdd/coach/conventions/session.convention.yaml +754 -0
- atdd/coach/overlays/__init__.py +2 -0
- atdd/coach/overlays/claude.md +2 -0
- atdd/coach/schemas/config.schema.json +34 -0
- atdd/coach/schemas/manifest.schema.json +101 -0
- atdd/coach/templates/ATDD.md +282 -0
- atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
- atdd/coach/utils/__init__.py +0 -0
- atdd/coach/utils/graph/__init__.py +0 -0
- atdd/coach/utils/graph/urn.py +875 -0
- atdd/coach/validators/__init__.py +0 -0
- atdd/coach/validators/shared_fixtures.py +365 -0
- atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
- atdd/coach/validators/test_registry.py +575 -0
- atdd/coach/validators/test_session_validation.py +1183 -0
- atdd/coach/validators/test_traceability.py +448 -0
- atdd/coach/validators/test_update_feature_paths.py +108 -0
- atdd/coach/validators/test_validate_contract_consumers.py +297 -0
- atdd/coder/__init__.py +1 -0
- atdd/coder/conventions/adapter.recipe.yaml +88 -0
- atdd/coder/conventions/backend.convention.yaml +460 -0
- atdd/coder/conventions/boundaries.convention.yaml +666 -0
- atdd/coder/conventions/commons.convention.yaml +460 -0
- atdd/coder/conventions/complexity.recipe.yaml +109 -0
- atdd/coder/conventions/component-naming.convention.yaml +178 -0
- atdd/coder/conventions/design.convention.yaml +327 -0
- atdd/coder/conventions/design.recipe.yaml +273 -0
- atdd/coder/conventions/dto.convention.yaml +660 -0
- atdd/coder/conventions/frontend.convention.yaml +542 -0
- atdd/coder/conventions/green.convention.yaml +1012 -0
- atdd/coder/conventions/presentation.convention.yaml +587 -0
- atdd/coder/conventions/refactor.convention.yaml +535 -0
- atdd/coder/conventions/technology.convention.yaml +206 -0
- atdd/coder/conventions/tests/__init__.py +0 -0
- atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
- atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
- atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
- atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
- atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
- atdd/coder/conventions/thinness.recipe.yaml +82 -0
- atdd/coder/conventions/train.convention.yaml +325 -0
- atdd/coder/conventions/verification.protocol.yaml +53 -0
- atdd/coder/schemas/design_system.schema.json +361 -0
- atdd/coder/validators/__init__.py +0 -0
- atdd/coder/validators/test_commons_structure.py +485 -0
- atdd/coder/validators/test_complexity.py +416 -0
- atdd/coder/validators/test_cross_language_consistency.py +431 -0
- atdd/coder/validators/test_design_system_compliance.py +413 -0
- atdd/coder/validators/test_dto_testing_patterns.py +268 -0
- atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
- atdd/coder/validators/test_green_layer_dependencies.py +148 -0
- atdd/coder/validators/test_green_python_layer_structure.py +103 -0
- atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
- atdd/coder/validators/test_import_boundaries.py +396 -0
- atdd/coder/validators/test_init_file_urns.py +593 -0
- atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
- atdd/coder/validators/test_presentation_convention.py +260 -0
- atdd/coder/validators/test_python_architecture.py +674 -0
- atdd/coder/validators/test_quality_metrics.py +420 -0
- atdd/coder/validators/test_station_master_pattern.py +244 -0
- atdd/coder/validators/test_train_infrastructure.py +454 -0
- atdd/coder/validators/test_train_urns.py +293 -0
- atdd/coder/validators/test_typescript_architecture.py +616 -0
- atdd/coder/validators/test_usecase_structure.py +421 -0
- atdd/coder/validators/test_wagon_boundaries.py +586 -0
- atdd/conftest.py +126 -0
- atdd/planner/__init__.py +1 -0
- atdd/planner/conventions/acceptance.convention.yaml +538 -0
- atdd/planner/conventions/appendix.convention.yaml +187 -0
- atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
- atdd/planner/conventions/component.convention.yaml +670 -0
- atdd/planner/conventions/criteria.convention.yaml +141 -0
- atdd/planner/conventions/feature.convention.yaml +371 -0
- atdd/planner/conventions/interface.convention.yaml +382 -0
- atdd/planner/conventions/steps.convention.yaml +141 -0
- atdd/planner/conventions/train.convention.yaml +552 -0
- atdd/planner/conventions/wagon.convention.yaml +275 -0
- atdd/planner/conventions/wmbt.convention.yaml +258 -0
- atdd/planner/schemas/acceptance.schema.json +336 -0
- atdd/planner/schemas/appendix.schema.json +78 -0
- atdd/planner/schemas/component.schema.json +114 -0
- atdd/planner/schemas/feature.schema.json +197 -0
- atdd/planner/schemas/train.schema.json +192 -0
- atdd/planner/schemas/wagon.schema.json +281 -0
- atdd/planner/schemas/wmbt.schema.json +59 -0
- atdd/planner/validators/__init__.py +0 -0
- atdd/planner/validators/conftest.py +5 -0
- atdd/planner/validators/test_draft_wagon_registry.py +374 -0
- atdd/planner/validators/test_plan_cross_refs.py +240 -0
- atdd/planner/validators/test_plan_uniqueness.py +224 -0
- atdd/planner/validators/test_plan_urn_resolution.py +268 -0
- atdd/planner/validators/test_plan_wagons.py +174 -0
- atdd/planner/validators/test_train_validation.py +514 -0
- atdd/planner/validators/test_wagon_urn_chain.py +648 -0
- atdd/planner/validators/test_wmbt_consistency.py +327 -0
- atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
- atdd/tester/__init__.py +1 -0
- atdd/tester/conventions/artifact.convention.yaml +257 -0
- atdd/tester/conventions/contract.convention.yaml +1009 -0
- atdd/tester/conventions/filename.convention.yaml +555 -0
- atdd/tester/conventions/migration.convention.yaml +509 -0
- atdd/tester/conventions/red.convention.yaml +797 -0
- atdd/tester/conventions/routing.convention.yaml +51 -0
- atdd/tester/conventions/telemetry.convention.yaml +458 -0
- atdd/tester/schemas/a11y.tmpl.json +17 -0
- atdd/tester/schemas/artifact.schema.json +189 -0
- atdd/tester/schemas/contract.schema.json +591 -0
- atdd/tester/schemas/contract.tmpl.json +95 -0
- atdd/tester/schemas/db.tmpl.json +20 -0
- atdd/tester/schemas/e2e.tmpl.json +17 -0
- atdd/tester/schemas/edge_function.tmpl.json +17 -0
- atdd/tester/schemas/event.tmpl.json +17 -0
- atdd/tester/schemas/http.tmpl.json +19 -0
- atdd/tester/schemas/job.tmpl.json +18 -0
- atdd/tester/schemas/load.tmpl.json +21 -0
- atdd/tester/schemas/metric.tmpl.json +19 -0
- atdd/tester/schemas/pack.schema.json +139 -0
- atdd/tester/schemas/realtime.tmpl.json +20 -0
- atdd/tester/schemas/rls.tmpl.json +18 -0
- atdd/tester/schemas/script.tmpl.json +16 -0
- atdd/tester/schemas/sec.tmpl.json +18 -0
- atdd/tester/schemas/storage.tmpl.json +18 -0
- atdd/tester/schemas/telemetry.schema.json +128 -0
- atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
- atdd/tester/schemas/test_filename.schema.json +194 -0
- atdd/tester/schemas/test_intent.schema.json +179 -0
- atdd/tester/schemas/unit.tmpl.json +18 -0
- atdd/tester/schemas/visual.tmpl.json +18 -0
- atdd/tester/schemas/ws.tmpl.json +17 -0
- atdd/tester/utils/__init__.py +0 -0
- atdd/tester/utils/filename.py +300 -0
- atdd/tester/validators/__init__.py +0 -0
- atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
- atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
- atdd/tester/validators/conftest.py +5 -0
- atdd/tester/validators/coverage_gap_report.py +321 -0
- atdd/tester/validators/fix_dual_ac_references.py +179 -0
- atdd/tester/validators/remove_duplicate_lines.py +93 -0
- atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
- atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
- atdd/tester/validators/test_artifact_naming_category.py +307 -0
- atdd/tester/validators/test_contract_schema_compliance.py +706 -0
- atdd/tester/validators/test_contracts_structure.py +200 -0
- atdd/tester/validators/test_coverage_adequacy.py +797 -0
- atdd/tester/validators/test_dual_ac_reference.py +225 -0
- atdd/tester/validators/test_fixture_validity.py +372 -0
- atdd/tester/validators/test_isolation.py +487 -0
- atdd/tester/validators/test_migration_coverage.py +204 -0
- atdd/tester/validators/test_migration_criteria.py +276 -0
- atdd/tester/validators/test_migration_generation.py +116 -0
- atdd/tester/validators/test_python_test_naming.py +410 -0
- atdd/tester/validators/test_red_layer_validation.py +95 -0
- atdd/tester/validators/test_red_python_layer_structure.py +87 -0
- atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
- atdd/tester/validators/test_telemetry_structure.py +634 -0
- atdd/tester/validators/test_typescript_test_naming.py +301 -0
- atdd/tester/validators/test_typescript_test_structure.py +84 -0
- atdd-0.1.0.dist-info/METADATA +191 -0
- atdd-0.1.0.dist-info/RECORD +183 -0
- atdd-0.1.0.dist-info/WHEEL +5 -0
- atdd-0.1.0.dist-info/entry_points.txt +2 -0
- atdd-0.1.0.dist-info/licenses/LICENSE +674 -0
- 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()
|