Files
KSP_project/scripts/migrate_state_labels.py
ArmorandClaude Opus 4.7 e1d601a774 chore: add reference docs, scripts, tests, demo, prototypes
- Reference docs: known issues, user journey, location redesign, mission merge plan
- Scripts: fill_location_state, merge_missions, migrate_location, migrate_state
- Tests: e2e test suite, asset entries unit tests
- Demo: location board, hifi prototypes, test screenshots
- Data backup: asset log entries and state nodes
- Update .gitignore

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 15:29:47 +08:00

165 lines
6.9 KiB
Python

"""
Migrate state labels: Transfer/In Transit -> start-transit / transit-complete.
Set previous_location and transit_location on start-transit nodes.
"""
import os, psycopg, re
from datetime import datetime, timezone
from dotenv import load_dotenv
from pathlib import Path
load_dotenv(Path(__file__).resolve().parents[1] / ".env")
conn = psycopg.connect(
host=os.getenv("PGHOST"), port=os.getenv("PGPORT"),
dbname=os.getenv("PGDATABASE"), user=os.getenv("PGUSER"),
password=os.getenv("PGPASSWORD"))
cur = conn.cursor()
NOW = datetime.now(timezone.utc)
# 1. Fetch all state nodes with their parent entry info, ordered by time
cur.execute("""
SELECT sn.id::text, sn.title, sn.detail, sn.at_time, sn.location, sn.state_label,
le.title as entry_title, le.mission_label, le.start_at, le.end_at,
a.name as asset_name
FROM asset_state_nodes sn
JOIN asset_log_entries le ON sn.entry_id = le.id
JOIN assets a ON le.asset_id = a.id
ORDER BY a.name, le.start_at, sn.at_time
""")
rows = cur.fetchall()
print(f"Processing {len(rows)} state nodes")
# Group state nodes by entry
from collections import defaultdict
entries = defaultdict(list)
for r in rows:
eid = (r[6], r[7], r[8]) # entry_title, mission_label, start_at
entries[eid].append(r)
updated = 0
for ekey, nodes in entries.items():
entry_title, mission_label, entry_start = ekey
# Find pairs: departure -> arrival within this entry
# A departure has keywords: depart, departure, leave, outbound
# An arrival has keywords: arriv, arrival, inbound, return
for i, node in enumerate(nodes):
nid, ntitle, ndetail, ntime, nloc, nst, *_ = node
title_lower = (ntitle or "").lower()
detail_lower = (ndetail or "").lower()
ctx = f"{title_lower} {detail_lower}"
new_st = None
new_prev_loc = None
new_transit_loc = None
is_departure = any(w in ctx for w in
["departure", "depart", "leave", "outbound", "departed"])
is_arrival = any(w in ctx for w in
["arrival", "arriv", "arrive", "inbound", "return", "soi"])
if is_departure and is_arrival:
# Ambiguous - use position context. If it mentions Earth or a body + departure, it's departure
if "departure" in ctx or "depart" in ctx or "leave" in ctx:
is_arrival = False
else:
is_departure = False
if is_departure:
new_st = "start-transit"
new_transit_loc = "Solar Orbit"
elif is_arrival:
new_st = "transit-complete"
# Also check existing state_label
if nst and nst.lower() in ("transfer", "in transit"):
if not new_st:
# Fallback: try to infer from title
if any(w in ctx for w in ["departure", "depart", "leave"]):
new_st = "start-transit"
new_transit_loc = "Solar Orbit"
elif any(w in ctx for w in ["arrival", "arriv", "arrive"]):
new_st = "transit-complete"
else:
# Keep as Transfer if we can't determine
new_st = "start-transit"
new_transit_loc = "Solar Orbit"
if new_st:
# Determine previous_location for start-transit
if new_st == "start-transit":
# Get previous location from:
# a) Entry-level context (where is the asset before this mission?)
# b) Explicit departure body
body_match = re.search(
r'\b(earth|mars|europa|jupiter|saturn|venus|mercury|neptune|uranus|vesta|ceres|titan|io|ganymede|callisto|triton|pluto|leo|geo)\b',
ctx
)
if body_match:
body = body_match.group(1)
body_map = {
"earth": "LEO", "leo": "LEO", "geo": "GEO",
"mars": "Mars Orbit", "venus": "Venus Orbit",
"mercury": "Mercury Orbit", "jupiter": "Jupiter System",
"europa": "Europa Orbit", "io": "Io Orbit",
"ganymede": "Ganymede Orbit", "callisto": "Callisto Orbit",
"saturn": "Saturn System", "titan": "Titan Orbit",
"uranus": "Uranus System", "neptune": "Neptune System",
"triton": "Triton Orbit", "vesta": "Vesta Orbit",
"ceres": "Ceres Orbit", "pluto": "Pluto Orbit",
}
new_prev_loc = body_map.get(body, "LEO")
else:
new_prev_loc = nloc or "LEO"
# transit_location
if not new_transit_loc:
new_transit_loc = "Solar Orbit"
# For transit-complete, the location IS the arrival location
if new_st == "transit-complete" and not nloc:
# Try to infer arrival location from title
body_match = re.search(
r'\b(earth|mars|europa|jupiter|saturn|venus|mercury|neptune|uranus|vesta|titan|io|ganymede|callisto|triton)\b',
ctx
)
if body_match:
body = body_match.group(1)
body_map = {
"earth": "LEO", "mars": "Mars Orbit", "venus": "Venus Orbit",
"mercury": "Mercury Orbit", "jupiter": "Jupiter System",
"europa": "Europa Orbit", "io": "Io Orbit",
"ganymede": "Ganymede Orbit", "callisto": "Callisto Orbit",
"saturn": "Saturn System", "titan": "Titan Orbit",
"uranus": "Uranus System", "neptune": "Neptune System",
"triton": "Triton Orbit", "vesta": "Vesta Orbit",
}
nloc = body_map.get(body, nloc)
cur.execute("""
UPDATE asset_state_nodes SET
state_label = %s,
previous_location = COALESCE(%s, previous_location),
transit_location = COALESCE(%s, transit_location),
location = COALESCE(%s, location),
updated_at = %s
WHERE id = %s
""", (new_st, new_prev_loc, new_transit_loc, nloc, NOW, nid))
updated += 1
conn.commit()
print(f"Updated {updated} state nodes with new state labels")
# Stats
cur.execute("SELECT state_label, COUNT(*) FROM asset_state_nodes GROUP BY state_label ORDER BY COUNT(*) DESC")
print("\nState label distribution:")
for r in cur.fetchall():
print(f" {r[0] or 'NULL':25s}: {r[1]}")
cur.execute("SELECT COUNT(*), COUNT(CASE WHEN previous_location IS NOT NULL THEN 1 END), COUNT(CASE WHEN transit_location IS NOT NULL THEN 1 END) FROM asset_state_nodes")
r = cur.fetchone()
print(f"\nTotal: {r[0]}, with prev_loc: {r[1]}, with transit_loc: {r[2]}")
conn.close()