atdd 0.2.1__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 +6 -0
- atdd/__main__.py +4 -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.2.1.dist-info/METADATA +221 -0
- atdd-0.2.1.dist-info/RECORD +184 -0
- atdd-0.2.1.dist-info/WHEEL +5 -0
- atdd-0.2.1.dist-info/entry_points.txt +2 -0
- atdd-0.2.1.dist-info/licenses/LICENSE +674 -0
- atdd-0.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Generate Supabase JSONB migrations from contract schemas.
|
|
4
|
+
|
|
5
|
+
SPEC-COACH-CONV-0033: Simplified JSONB-only migration generator
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python atdd/coach/commands/migration.py # Generate all missing
|
|
9
|
+
python atdd/coach/commands/migration.py --contract <path> # Generate specific
|
|
10
|
+
python atdd/coach/commands/migration.py --validate # Check coverage only
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Path constants
|
|
20
|
+
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
21
|
+
CONTRACTS_DIR = REPO_ROOT / "contracts"
|
|
22
|
+
MIGRATIONS_DIR = REPO_ROOT / "supabase" / "migrations"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def contract_needs_migration(contract_path: Path) -> bool:
|
|
26
|
+
"""
|
|
27
|
+
Determine if a contract needs a database migration.
|
|
28
|
+
|
|
29
|
+
Decision algorithm (ordered rules, first match wins):
|
|
30
|
+
1. Explicit persistence.strategy: check if != 'none'
|
|
31
|
+
2. Empty properties: len(properties) == 0 ā NO
|
|
32
|
+
3. Event without id: aspect ends with '*ed' AND no 'id' ā NO
|
|
33
|
+
4. Internal only: metadata.to == 'internal' ā NO
|
|
34
|
+
5. Entity with id: 'id' in properties ā YES
|
|
35
|
+
6. Computed without id: description contains compute keywords AND no 'id' ā NO
|
|
36
|
+
7. Conservative default: metadata.to == 'external' AND has properties ā YES
|
|
37
|
+
8. Fallback: NO
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
with open(contract_path, 'r') as f:
|
|
41
|
+
contract = json.load(f)
|
|
42
|
+
|
|
43
|
+
metadata = contract.get("x-artifact-metadata", {})
|
|
44
|
+
properties = contract.get("properties", {})
|
|
45
|
+
description = contract.get("description", "").lower()
|
|
46
|
+
|
|
47
|
+
# Extract aspect name from path
|
|
48
|
+
aspect = contract_path.stem.replace(".schema", "")
|
|
49
|
+
|
|
50
|
+
# Rule 1: Check persistence metadata
|
|
51
|
+
persistence = metadata.get("persistence", {})
|
|
52
|
+
if persistence.get("strategy") == "none":
|
|
53
|
+
return False
|
|
54
|
+
elif persistence.get("strategy") in ["jsonb", "relational"]:
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
# Rule 2: Empty properties
|
|
58
|
+
if len(properties) == 0:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
# Rule 3: Event without id
|
|
62
|
+
has_id = "id" in properties
|
|
63
|
+
is_event_pattern = aspect.endswith("ed")
|
|
64
|
+
if is_event_pattern and not has_id:
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
# Rule 4: Internal only
|
|
68
|
+
if metadata.get("to") == "internal":
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
# Rule 5: Entity with id
|
|
72
|
+
if has_id:
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
# Rule 6: Computed without id
|
|
76
|
+
compute_keywords = ["computed", "calculated", "derived", "aggregated", "aggregate"]
|
|
77
|
+
is_computed = any(keyword in description for keyword in compute_keywords)
|
|
78
|
+
if is_computed and not has_id:
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
# Rule 7: Conservative default
|
|
82
|
+
if metadata.get("to") == "external" and len(properties) > 0:
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
# Rule 8: Fallback
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
print(f"Warning: Could not parse {contract_path}: {e}")
|
|
90
|
+
return True # Conservative: assume needs migration
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def derive_table_name_from_contract(contract_path: Path) -> str:
|
|
94
|
+
"""Derive table name from contract path: {theme}_{domain}_{aspect}"""
|
|
95
|
+
# Try relative to CONTRACTS_DIR first
|
|
96
|
+
try:
|
|
97
|
+
relative_path = contract_path.relative_to(CONTRACTS_DIR)
|
|
98
|
+
parts = relative_path.parts
|
|
99
|
+
except ValueError:
|
|
100
|
+
# Fallback: parse from path structure (for tests)
|
|
101
|
+
# Assume path is .../contracts/{theme}/{domain}/{aspect}.schema.json
|
|
102
|
+
parts = contract_path.parts
|
|
103
|
+
contracts_idx = parts.index("contracts") if "contracts" in parts else -4
|
|
104
|
+
parts = parts[contracts_idx + 1:]
|
|
105
|
+
|
|
106
|
+
theme = parts[0]
|
|
107
|
+
domain = parts[1]
|
|
108
|
+
aspect = contract_path.stem.replace(".schema", "")
|
|
109
|
+
|
|
110
|
+
return f"{theme}_{domain}_{aspect}".replace("-", "_")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def generate_migration_sql(contract_path: Path) -> str:
|
|
114
|
+
"""
|
|
115
|
+
Generate standard JSONB blob table migration.
|
|
116
|
+
|
|
117
|
+
All tables use same structure:
|
|
118
|
+
- id UUID PRIMARY KEY
|
|
119
|
+
- data JSONB NOT NULL (stores entire contract)
|
|
120
|
+
- created_at, updated_at timestamps
|
|
121
|
+
- GIN index on data
|
|
122
|
+
- Table comment with contract $id for traceability
|
|
123
|
+
"""
|
|
124
|
+
with open(contract_path, 'r') as f:
|
|
125
|
+
contract = json.load(f)
|
|
126
|
+
|
|
127
|
+
table_name = derive_table_name_from_contract(contract_path)
|
|
128
|
+
contract_id = contract.get("$id", "unknown")
|
|
129
|
+
|
|
130
|
+
# Standard JSONB table template
|
|
131
|
+
try:
|
|
132
|
+
path_display = str(contract_path.relative_to(REPO_ROOT))
|
|
133
|
+
except ValueError:
|
|
134
|
+
path_display = contract_path.name
|
|
135
|
+
|
|
136
|
+
sql = f"""-- Generated from {path_display}
|
|
137
|
+
-- Contract: {contract_id}
|
|
138
|
+
-- Generated: {datetime.now().isoformat()}
|
|
139
|
+
-- JSONB-first storage strategy (see migration.convention.yaml)
|
|
140
|
+
|
|
141
|
+
CREATE TABLE IF NOT EXISTS {table_name} (
|
|
142
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
143
|
+
data JSONB NOT NULL,
|
|
144
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
145
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
CREATE INDEX idx_{table_name}_data ON {table_name} USING GIN (data);
|
|
149
|
+
|
|
150
|
+
COMMENT ON TABLE {table_name} IS 'Contract: {contract_id}. JSONB blob storage for wagon architecture.';
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
return sql
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def main():
|
|
157
|
+
parser = argparse.ArgumentParser(description="Generate Supabase JSONB migrations from contracts")
|
|
158
|
+
parser.add_argument("--contract", type=Path, help="Specific contract to generate migration for")
|
|
159
|
+
parser.add_argument("--validate", action="store_true", help="Only validate coverage, don't generate")
|
|
160
|
+
|
|
161
|
+
args = parser.parse_args()
|
|
162
|
+
|
|
163
|
+
MIGRATIONS_DIR.mkdir(parents=True, exist_ok=True)
|
|
164
|
+
|
|
165
|
+
if args.validate:
|
|
166
|
+
print("Validating migration coverage...")
|
|
167
|
+
print("Run: pytest atdd/tester/test_migration_coverage.py")
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
# Generate for specific contract
|
|
171
|
+
if args.contract:
|
|
172
|
+
if not args.contract.exists():
|
|
173
|
+
print(f"Error: Contract not found: {args.contract}")
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
if not contract_needs_migration(args.contract):
|
|
177
|
+
print(f"ā¹ļø Contract does not need migration (strategy='none' or transient)")
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
migration_sql = generate_migration_sql(args.contract)
|
|
181
|
+
table_name = derive_table_name_from_contract(args.contract)
|
|
182
|
+
|
|
183
|
+
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
184
|
+
filename = f"{timestamp}_{table_name}.sql"
|
|
185
|
+
output_path = MIGRATIONS_DIR / filename
|
|
186
|
+
|
|
187
|
+
output_path.write_text(migration_sql)
|
|
188
|
+
print(f"ā
Generated: {output_path.relative_to(REPO_ROOT)}")
|
|
189
|
+
print(f"š¦ JSONB blob storage - no manual review needed")
|
|
190
|
+
print(f"š Apply: supabase db push")
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
# Generate all missing migrations
|
|
194
|
+
print("Scanning contracts for missing migrations...")
|
|
195
|
+
|
|
196
|
+
contracts = list(CONTRACTS_DIR.rglob("*.schema.json"))
|
|
197
|
+
generated = 0
|
|
198
|
+
skipped = 0
|
|
199
|
+
|
|
200
|
+
for contract in contracts:
|
|
201
|
+
relative_path = contract.relative_to(CONTRACTS_DIR)
|
|
202
|
+
parts = relative_path.parts
|
|
203
|
+
|
|
204
|
+
if len(parts) < 3:
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
if not contract_needs_migration(contract):
|
|
208
|
+
skipped += 1
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
table_name = derive_table_name_from_contract(contract)
|
|
212
|
+
|
|
213
|
+
# Check if migration exists
|
|
214
|
+
has_migration = any(
|
|
215
|
+
f"CREATE TABLE {table_name}" in mig.read_text() or
|
|
216
|
+
f"CREATE TABLE IF NOT EXISTS {table_name}" in mig.read_text()
|
|
217
|
+
for mig in MIGRATIONS_DIR.glob("*.sql")
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if not has_migration:
|
|
221
|
+
migration_sql = generate_migration_sql(contract)
|
|
222
|
+
|
|
223
|
+
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
224
|
+
filename = f"{timestamp}_{table_name}.sql"
|
|
225
|
+
output_path = MIGRATIONS_DIR / filename
|
|
226
|
+
|
|
227
|
+
output_path.write_text(migration_sql)
|
|
228
|
+
print(f"ā
Generated: {output_path.relative_to(REPO_ROOT)}")
|
|
229
|
+
generated += 1
|
|
230
|
+
|
|
231
|
+
print(f"\nā
Generated {generated} JSONB migrations")
|
|
232
|
+
if skipped > 0:
|
|
233
|
+
print(f"ā¹ļø Skipped {skipped} non-persistent contracts")
|
|
234
|
+
print(f"š¦ All use standard JSONB blob storage")
|
|
235
|
+
print(f"š Apply: supabase db push")
|
|
236
|
+
print(f"\nValidate: pytest atdd/tester/test_migration_coverage.py")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
if __name__ == "__main__":
|
|
240
|
+
main()
|