Skip to content

Commit ee6ef54

Browse files
committed
Add deterministic phase 2 catalog migration
1 parent ad89bbc commit ee6ef54

1 file changed

Lines changed: 133 additions & 0 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#!/usr/bin/env python3
2+
"""Complete settings-catalog.json coverage for the Phase 2 settings refactor.
3+
4+
This is intentionally a one-shot, deterministic migration. It refuses to run if the
5+
set of currently uncataloged persistent keys differs from the baseline discovered
6+
in Phase 1, so it cannot silently paper over future catalog regressions.
7+
"""
8+
from __future__ import annotations
9+
10+
import json
11+
from pathlib import Path
12+
from typing import Any, Dict
13+
14+
import settings_audit
15+
16+
ROOT = Path(__file__).resolve().parents[1]
17+
CATALOG = ROOT / settings_audit.CATALOG_PATH
18+
19+
INTERNAL_KEYS = {
20+
"garmin_expires_at",
21+
"garmin_refresh_token_expires_at",
22+
"service_changed",
23+
}
24+
25+
METADATA: Dict[str, Dict[str, Any]] = {
26+
"ant_garmin": {"name": "Garmin ANT+", "parent": "Garmin Options"},
27+
"garmin_expires_at": {"name": "Garmin Access Token Expiry", "parent": "Garmin Options"},
28+
"garmin_refresh_token_expires_at": {"name": "Garmin Refresh Token Expiry", "parent": "Garmin Options"},
29+
"peloton_bike_ocr": {"name": "Peloton Bike OCR", "parent": "Peloton Options"},
30+
"service_changed": {"name": "Bluetooth Service Changed State", "parent": "Advanced Settings"},
31+
"watt_bike_emulator": {"name": "Wattbike Emulator", "parent": "Bike Options"},
32+
"mywhoosh_link_enabled": {"name": "Enable OpenBikeControl", "parent": "OpenBikeControl"},
33+
"mywhoosh_link_override_gears": {"name": "OpenBikeControl Override Gears", "parent": "OpenBikeControl"},
34+
"mywhoosh_link_left_up": {"name": "Left Controller Up", "parent": "OpenBikeControl"},
35+
"mywhoosh_link_left_down": {"name": "Left Controller Down", "parent": "OpenBikeControl"},
36+
"mywhoosh_link_left_left": {"name": "Left Controller Left", "parent": "OpenBikeControl"},
37+
"mywhoosh_link_left_right": {"name": "Left Controller Right", "parent": "OpenBikeControl"},
38+
"mywhoosh_link_left_power": {"name": "Left Controller Power", "parent": "OpenBikeControl"},
39+
"mywhoosh_link_left_shoulder": {"name": "Left Controller Shoulder", "parent": "OpenBikeControl"},
40+
"mywhoosh_link_right_a": {"name": "Right Controller A", "parent": "OpenBikeControl"},
41+
"mywhoosh_link_right_b": {"name": "Right Controller B", "parent": "OpenBikeControl"},
42+
"mywhoosh_link_right_y": {"name": "Right Controller Y", "parent": "OpenBikeControl"},
43+
"mywhoosh_link_right_z": {"name": "Right Controller Z", "parent": "OpenBikeControl"},
44+
"mywhoosh_link_right_power": {"name": "Right Controller Power", "parent": "OpenBikeControl"},
45+
"mywhoosh_link_right_shoulder": {"name": "Right Controller Shoulder", "parent": "OpenBikeControl"},
46+
"mywhoosh_link_camera_value": {"name": "Camera Action", "parent": "OpenBikeControl"},
47+
"mywhoosh_link_emote_value": {"name": "Emote Action", "parent": "OpenBikeControl"},
48+
"zwiftplay_gear_paddle_left": {"name": "Left Paddle Gear Action", "parent": "Zwift Play Options"},
49+
"zwiftplay_gear_paddle_right": {"name": "Right Paddle Gear Action", "parent": "Zwift Play Options"},
50+
"zwiftplay_gear_lb": {"name": "Left Button Gear Action", "parent": "Zwift Play Options"},
51+
"zwiftplay_gear_rb": {"name": "Right Button Gear Action", "parent": "Zwift Play Options"},
52+
"zwiftplay_gear_ls1": {"name": "Left Shift 1 Gear Action", "parent": "Zwift Play Options"},
53+
"zwiftplay_gear_ls2": {"name": "Left Shift 2 Gear Action", "parent": "Zwift Play Options"},
54+
"zwiftplay_gear_rs1": {"name": "Right Shift 1 Gear Action", "parent": "Zwift Play Options"},
55+
"zwiftplay_gear_rs2": {"name": "Right Shift 2 Gear Action", "parent": "Zwift Play Options"},
56+
}
57+
58+
EXPECTED_MISSING = set(METADATA)
59+
60+
61+
def parse_default(qml_type: str, expression: str) -> Any:
62+
expr = expression.strip()
63+
if qml_type == "bool":
64+
if expr == "true":
65+
return True
66+
if expr == "false":
67+
return False
68+
if qml_type == "int":
69+
return int(float(expr))
70+
if qml_type == "real":
71+
return float(expr)
72+
if qml_type == "string" and len(expr) >= 2 and expr[0] == expr[-1] == '"':
73+
return bytes(expr[1:-1], "utf-8").decode("unicode_escape")
74+
return None
75+
76+
77+
def catalog_type(qml_type: str) -> str:
78+
return {
79+
"bool": "boolean",
80+
"int": "integer",
81+
"real": "number",
82+
"string": "string",
83+
}.get(qml_type, "string")
84+
85+
86+
def main() -> int:
87+
catalog = json.loads(CATALOG.read_text(encoding="utf-8"))
88+
declarations = settings_audit.flatten(settings_audit.declarations_from(ROOT))
89+
unique = {}
90+
for decl in declarations:
91+
unique.setdefault(decl.key, decl)
92+
93+
existing = {entry["key"] for entry in catalog.get("settings", [])}
94+
missing = set(unique) - existing
95+
if missing != EXPECTED_MISSING:
96+
raise SystemExit(
97+
"Refusing Phase 2 migration because the uncataloged baseline changed.\n"
98+
f"Expected: {sorted(EXPECTED_MISSING)}\n"
99+
f"Actual: {sorted(missing)}"
100+
)
101+
102+
for key in sorted(missing):
103+
decl = unique[key]
104+
meta = METADATA[key]
105+
qml_type = decl.qml_type
106+
entry = {
107+
"key": key,
108+
"name": meta["name"],
109+
"description": None,
110+
"parent": meta["parent"],
111+
"type": catalog_type(qml_type),
112+
"qmlType": qml_type,
113+
"control": "switch" if qml_type == "bool" else "text",
114+
"visible": key not in INTERNAL_KEYS,
115+
"defaultValue": parse_default(qml_type, decl.default_expression),
116+
"defaultExpression": settings_audit.normalize_expression(decl.default_expression),
117+
"options": None,
118+
}
119+
catalog["settings"].append(entry)
120+
121+
catalog["settingCount"] = len({entry["key"] for entry in catalog["settings"]})
122+
if catalog["settingCount"] != len(unique):
123+
raise SystemExit(
124+
f"Catalog migration did not reach full coverage: {catalog['settingCount']}/{len(unique)}"
125+
)
126+
127+
CATALOG.write_text(json.dumps(catalog, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
128+
print(f"Catalog migrated to {catalog['settingCount']}/{len(unique)} persistent keys")
129+
return 0
130+
131+
132+
if __name__ == "__main__":
133+
raise SystemExit(main())

0 commit comments

Comments
 (0)