"""Incrementally seed GHC-06 topology data without rebuilding earlier missions. Usage: python topology_api/seed/seed_ghc06.py """ import json from datetime import datetime, timezone from topology_api.db import write_cursor UTC = timezone.utc def ts(value): return datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(UTC) def seed(): with write_cursor() as cur: cur.execute("SELECT id FROM bases WHERE code = 'guanghan'") base = cur.fetchone() if not base: raise RuntimeError("Base 'guanghan' is missing; seed GHC-01 through GHC-05 first") base_id = base["id"] cur.execute(""" INSERT INTO missions (id, base_id, code, title, occurrence_status, description, source_ref) VALUES (gen_random_uuid(), %(base)s, 'GHC-06', '广寒基地建设任务-06', 'occurred', '第二次乘组轮换与广寒 04 北部扩展区建设', '/home/mission/Guanghan_Program/GHC-06') ON CONFLICT (base_id, code) DO UPDATE SET title = EXCLUDED.title, occurrence_status = EXCLUDED.occurrence_status, description = EXCLUDED.description, source_ref = EXCLUDED.source_ref, updated_at = now() RETURNING id """, {"base": base_id}) mission_id = cur.fetchone()["id"] event_specs = [ ("landing", "2025-01-11T10:49:00Z", 0, "component_introduced", "吴刚三号着陆广寒基地"), ("lander_connected", "2025-01-12T00:00:00Z", 0, "connection_established", "吴刚三号通过柔性加压管道接入基地"), ("hub_installed", "2025-02-15T00:00:00Z", 0, "component_installed", "十字安装模块安装至北部扩展位置"), ("greenhouse_installed", "2025-02-17T00:00:00Z", 0, "component_installed", "温室二号与堆肥模块安装完成"), ("habitat_storage_installed", "2025-02-19T00:00:00Z", 0, "component_installed", "存储模块与居住舱三号安装完成"), ("passage_installed", "2025-02-22T00:00:00Z", 0, "connection_established", "温室模块与温室二号加压走道安装完成"), ("topology_reconfigured", "2025-02-22T00:00:00Z", 1, "connection_changed", "吴刚二号改接实验舱远端接口"), ("reactor_35", "2025-02-27T00:00:00Z", 0, "state_changed", "核反应堆功率提升至 35%"), ("lander_departed", "2025-03-11T00:00:00Z", 0, "component_retired", "GHC-06 乘组搭乘吴刚三号撤离"), ("terminal", "2025-03-11T00:00:00Z", 1, "mission_checkpoint", "GHC-06 任务结束状态"), ] events = {} event_times = {} for key, effective_at, order, event_type, title in event_specs: event_times[key] = ts(effective_at) cur.execute(""" INSERT INTO mission_events (id, mission_id, effective_at, event_order, event_type, title, source_ref) VALUES (gen_random_uuid(), %(mission)s, %(at)s, %(ord)s, %(type)s, %(title)s, %(source)s) ON CONFLICT (mission_id, effective_at, event_order) DO UPDATE SET event_type = EXCLUDED.event_type, title = EXCLUDED.title, source_ref = EXCLUDED.source_ref RETURNING id """, { "mission": mission_id, "at": event_times[key], "ord": order, "type": event_type, "title": title, "source": "/home/mission/Guanghan_Program/GHC-06", }) events[key] = cur.fetchone()["id"] component_specs = [ ("wugang_3", "vehicle", "landing", "吴刚三号", "Block 2 载人月面着陆器", {"block": 2, "role": "crewed_lander"}), ("expansion_hub", "node", "hub_installed", "十字安装模块", "广寒 04 北部扩展区四向安装节点", {"dockingNodes": 4}), ("greenhouse_2", "module", "greenhouse_installed", "温室二号", "第二座折叠式月面温室", {"foldingArms": 4}), ("compost_module", "equipment", "greenhouse_installed", "堆肥模块", "温室二号有机废弃物与养分循环设备", {}), ("storage_module", "module", "habitat_storage_installed", "存储模块", "北部扩展区温湿度受控存储设施", {}), ("habitat_3", "module", "habitat_storage_installed", "居住舱段三号", "四铺位居住舱段,使基地容量提升至 14 人", {"capacity": 4}), ] components = {} for key, component_type, event_key, display_name, summary, metadata in component_specs: cur.execute(""" INSERT INTO components (id, base_id, component_key, component_type, introduced_by_event_id, metadata) VALUES (gen_random_uuid(), %(base)s, %(key)s, %(type)s, %(event)s, %(metadata)s) ON CONFLICT (base_id, component_key) DO UPDATE SET component_type = EXCLUDED.component_type, introduced_by_event_id = EXCLUDED.introduced_by_event_id, metadata = EXCLUDED.metadata, updated_at = now() RETURNING id """, { "base": base_id, "key": key, "type": component_type, "event": events[event_key], "metadata": json.dumps(metadata), }) component_id = cur.fetchone()["id"] components[key] = component_id cur.execute(""" SELECT id FROM component_versions WHERE component_id = %(component)s AND started_by_event_id = %(event)s """, {"component": component_id, "event": events[event_key]}) version = cur.fetchone() values = { "component": component_id, "name": display_name, "summary": summary, "properties": json.dumps(metadata), "event": events[event_key], } if version: cur.execute(""" UPDATE component_versions SET display_name = %(name)s, summary = %(summary)s, properties = %(properties)s, updated_at = now() WHERE id = %(id)s """, {**values, "id": version["id"]}) else: cur.execute(""" INSERT INTO component_versions (id, component_id, display_name, summary, properties, valid_from, started_by_event_id) VALUES (gen_random_uuid(), %(component)s, %(name)s, %(summary)s, %(properties)s, (SELECT effective_at FROM mission_events WHERE id = %(event)s), %(event)s) """, values) cur.execute(""" UPDATE components SET retired_by_event_id = %(event)s, updated_at = now() WHERE id = %(component)s """, {"event": events["lander_departed"], "component": components["wugang_3"]}) cur.execute(""" UPDATE component_versions SET valid_to = (SELECT effective_at FROM mission_events WHERE id = %(event)s), ended_by_event_id = %(event)s, updated_at = now() WHERE component_id = %(component)s AND valid_to IS NULL """, {"event": events["lander_departed"], "component": components["wugang_3"]}) # Include existing components used as attachment roots. cur.execute(""" SELECT component_key, id FROM components WHERE base_id = %(base)s AND component_key IN ( 'greenhouse', 'lab_module', 'wugang_2', 'nuclear_reactor' ) """, {"base": base_id}) components.update({row["component_key"]: row["id"] for row in cur.fetchall()}) for required in ("greenhouse", "lab_module", "wugang_2", "nuclear_reactor"): if required not in components: raise RuntimeError(f"Required component '{required}' is missing") port_specs = [ ("wugang_3", "dock", "加压对接口", "south", "docking"), ("expansion_hub", "north", "北部安装口", "north", "structural"), ("expansion_hub", "east", "东部安装口", "east", "structural"), ("expansion_hub", "south", "南部加压接口", "south", "pressurized"), ("expansion_hub", "west", "西部安装口", "west", "structural"), ("greenhouse_2", "core", "十字模块接口", "west", "structural"), ("greenhouse_2", "passage", "温室模块加压接口", "east", "pressurized"), ("greenhouse_2", "mount", "顶部设备接口", "north", "structural"), ("compost_module", "mount", "温室安装接口", "south", "structural"), ("storage_module", "core", "十字模块接口", "south", "structural"), ("habitat_3", "core", "十字模块接口", "east", "structural"), ("greenhouse", "distal", "远端对接口", "west", "pressurized"), ("lab_module", "distal", "远端加压接口", "east", "pressurized"), ("wugang_2", "dock", "加压对接口", "west", "docking"), ] ports = {} for component_key, port_key, name, direction, interface_type in port_specs: cur.execute(""" INSERT INTO ports (id, component_id, port_key, display_name, direction, interface_type) VALUES (gen_random_uuid(), %(component)s, %(key)s, %(name)s, %(direction)s, %(interface)s) ON CONFLICT (component_id, port_key) DO UPDATE SET display_name = EXCLUDED.display_name, direction = EXCLUDED.direction, interface_type = EXCLUDED.interface_type RETURNING id """, { "component": components[component_key], "key": port_key, "name": name, "direction": direction, "interface": interface_type, }) ports[(component_key, port_key)] = cur.fetchone()["id"] connection_specs = [ ("expansion_hub", "north", "storage_module", "core", "structural_mount", "habitat_storage_installed", None), ("expansion_hub", "east", "greenhouse_2", "core", "structural_mount", "greenhouse_installed", None), ("expansion_hub", "west", "habitat_3", "core", "structural_mount", "habitat_storage_installed", None), ("greenhouse_2", "mount", "compost_module", "mount", "structural_mount", "greenhouse_installed", None), ("greenhouse", "distal", "greenhouse_2", "passage", "pressurized_passage", "passage_installed", None), ("lab_module", "distal", "wugang_2", "dock", "pressurized_passage", "topology_reconfigured", None), ] # Remove the earlier inferred south connection; the GHC-06 terminal # topology keeps the expansion hub's south port intentionally vacant. cur.execute(""" DELETE FROM topology_connections tc USING ports pa, ports pb WHERE tc.port_a_id = pa.id AND tc.port_b_id = pb.id AND tc.established_by_event_id = %(event)s AND ( pa.component_id = %(hub)s OR pb.component_id = %(hub)s ) AND ( pa.component_id NOT IN (%(storage)s, %(greenhouse)s, %(habitat)s, %(compost)s) OR pb.component_id NOT IN (%(storage)s, %(greenhouse)s, %(habitat)s, %(compost)s) ) """, { "event": events["passage_installed"], "hub": components["expansion_hub"], "storage": components["storage_module"], "greenhouse": components["greenhouse_2"], "habitat": components["habitat_3"], "compost": components["compost_module"], }) # Close the old greenhouse-to-Wugang-2 passage when the lander moves # to the laboratory cabin's distal interface. cur.execute(""" UPDATE topology_connections tc SET valid_to = %(at)s, ended_by_event_id = %(event)s, updated_at = now() FROM ports pa, ports pb WHERE tc.port_a_id = pa.id AND tc.port_b_id = pb.id AND tc.valid_to IS NULL AND ((pa.component_id = %(greenhouse)s AND pb.component_id = %(wugang)s) OR (pa.component_id = %(wugang)s AND pb.component_id = %(greenhouse)s)) """, { "at": event_times["topology_reconfigured"], "event": events["topology_reconfigured"], "greenhouse": components["greenhouse"], "wugang": components["wugang_2"], }) for comp_a, port_a, comp_b, port_b, kind, start_key, end_key in connection_specs: port_a_id = ports[(comp_a, port_a)] port_b_id = ports[(comp_b, port_b)] cur.execute(""" SELECT id FROM topology_connections WHERE base_id = %(base)s AND port_a_id = %(port_a)s AND port_b_id = %(port_b)s AND established_by_event_id = %(event)s """, { "base": base_id, "port_a": port_a_id, "port_b": port_b_id, "event": events[start_key], }) connection = cur.fetchone() params = { "base": base_id, "port_a": port_a_id, "port_b": port_b_id, "kind": kind, "event": events[start_key], "end_event": events[end_key] if end_key else None, "end_at": event_times[end_key] if end_key else None, } if connection: cur.execute(""" UPDATE topology_connections SET connection_type = %(kind)s, valid_to = %(end_at)s, ended_by_event_id = %(end_event)s, updated_at = now() WHERE id = %(id)s """, {**params, "id": connection["id"]}) else: cur.execute(""" INSERT INTO topology_connections (id, base_id, port_a_id, port_b_id, connection_type, valid_from, valid_to, established_by_event_id, ended_by_event_id) VALUES ( gen_random_uuid(), %(base)s, %(port_a)s, %(port_b)s, %(kind)s, (SELECT effective_at FROM mission_events WHERE id = %(event)s), %(end_at)s, %(event)s, %(end_event)s) """, params) # Lifecycle states for newly introduced components. for key, _, event_key, _, _, _ in component_specs: cur.execute(""" SELECT id FROM component_states WHERE component_id = %(component)s AND state_kind = 'lifecycle' AND started_by_event_id = %(event)s """, {"component": components[key], "event": events[event_key]}) if not cur.fetchone(): cur.execute(""" INSERT INTO component_states (id, component_id, state_kind, state_value, valid_from, valid_to, started_by_event_id, ended_by_event_id) VALUES ( gen_random_uuid(), %(component)s, 'lifecycle', 'installed', (SELECT effective_at FROM mission_events WHERE id = %(event)s), %(end_at)s, %(event)s, %(end_event)s) """, { "component": components[key], "event": events[event_key], "end_event": events["lander_departed"] if key == "wugang_3" else None, "end_at": event_times["lander_departed"] if key == "wugang_3" else None, }) # Change the reactor state from 30% to 35% without creating overlaps. cur.execute(""" UPDATE component_states SET valid_to = (SELECT effective_at FROM mission_events WHERE id = %(event)s), ended_by_event_id = %(event)s, updated_at = now() WHERE component_id = %(component)s AND state_kind = 'operation' AND valid_to IS NULL AND details @> '{"powerPercent": 30}' """, {"component": components["nuclear_reactor"], "event": events["reactor_35"]}) cur.execute(""" SELECT id FROM component_states WHERE component_id = %(component)s AND state_kind = 'operation' AND started_by_event_id = %(event)s """, {"component": components["nuclear_reactor"], "event": events["reactor_35"]}) if not cur.fetchone(): cur.execute(""" INSERT INTO component_states (id, component_id, state_kind, state_value, details, valid_from, started_by_event_id) VALUES ( gen_random_uuid(), %(component)s, 'operation', 'operational', '{"powerPercent": 35}', (SELECT effective_at FROM mission_events WHERE id = %(event)s), %(event)s) """, {"component": components["nuclear_reactor"], "event": events["reactor_35"]}) # The version carries the headline engineering property used by the # card; create a new temporal version so older snapshots still show 30%. cur.execute(""" UPDATE component_versions SET valid_to = (SELECT effective_at FROM mission_events WHERE id = %(event)s), ended_by_event_id = %(event)s, updated_at = now() WHERE component_id = %(component)s AND valid_to IS NULL AND started_by_event_id <> %(event)s """, {"component": components["nuclear_reactor"], "event": events["reactor_35"]}) cur.execute(""" SELECT id FROM component_versions WHERE component_id = %(component)s AND started_by_event_id = %(event)s """, {"component": components["nuclear_reactor"], "event": events["reactor_35"]}) if not cur.fetchone(): cur.execute(""" INSERT INTO component_versions (id, component_id, display_name, summary, properties, valid_from, started_by_event_id) VALUES ( gen_random_uuid(), %(component)s, '核反应堆', '广寒基地主电力来源,GHC-06 后以百分之三十五功率运行', '{"powerPercent": 35}', (SELECT effective_at FROM mission_events WHERE id = %(event)s), %(event)s) """, {"component": components["nuclear_reactor"], "event": events["reactor_35"]}) print("GHC-06 topology seed complete") print(f" mission: GHC-06") print(f" events: {len(events)}") print(f" new components: {len(component_specs)}") print(f" connections: {len(connection_specs)}") if __name__ == "__main__": seed()