diff --git a/docs/superpowers/plans/2026-07-09-guanghan-topology.md b/docs/superpowers/plans/2026-07-09-guanghan-topology.md new file mode 100644 index 0000000..42cbb16 --- /dev/null +++ b/docs/superpowers/plans/2026-07-09-guanghan-topology.md @@ -0,0 +1,594 @@ +# Guanghan GHC-05 Topology Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the inaccurate Guanghan topology demo with a responsive, interactive single-canvas engineering diagram and correct the related GHC-05 timeline records. + +**Architecture:** Keep the deliverable as one standalone HTML file. Render the topology as an inline SVG with fixed semantic node IDs and connection types, keep module facts in one `MODULE_DETAILS` JavaScript object, and populate one accessible modal from that object. Add Python contract tests for source-level topology facts and a Playwright test for rendered pixels and modal behavior. + +**Tech Stack:** HTML5, CSS, inline SVG, vanilla JavaScript, Python `unittest`, Python `http.server`, Playwright. + +--- + +## File Structure + +- Create `tests/test_guanghan_topology_demo.py`: static contract tests for node inventory, connection types, future-extension comments, dimensions, and corrected timeline facts. +- Create `tests/test_guanghan_topology_e2e.py`: browser test for visible green corridors, hidden future markers, click targets, and modal closing behavior. +- Modify `data/wiki/guanghan_topology_demo.html`: complete standalone diagram, centralized details data, and accessible modal. +- Modify `user_input/广寒计划/Timeline.md`: remove the nonexistent ring corridor and correct all dependent GHC-05 records. + +### Task 1: Add failing source-contract tests + +**Files:** +- Create: `tests/test_guanghan_topology_demo.py` +- Read: `docs/superpowers/specs/2026-07-09-guanghan-topology-design.md` + +- [ ] **Step 1: Write the contract test** + +```python +from __future__ import annotations + +import re +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +HTML_PATH = ROOT / "data" / "wiki" / "guanghan_topology_demo.html" +TIMELINE_PATH = ROOT / "user_input" / "广寒计划" / "Timeline.md" + +CLICKABLE_MODULES = { + "core", + "habitat-1", + "habitat-2", + "greenhouse", + "laboratory", + "power-node", + "power-north", + "power-east", + "power-south", + "power-west", + "wugang-2", + "binglun-1", + "binglun-2", + "binglun-3", +} + + +class GuanghanTopologySourceTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.html = HTML_PATH.read_text(encoding="utf-8") + timeline = TIMELINE_PATH.read_text(encoding="utf-8") + cls.ghc05 = timeline.split("GHC-05任务", 1)[1].split("JC-02任务", 1)[0] + + def test_clickable_module_inventory_is_exact(self) -> None: + found = set(re.findall(r'data-module="([^"]+)"', self.html)) + self.assertEqual(found, CLICKABLE_MODULES) + + def test_connection_inventory_matches_approved_topology(self) -> None: + self.assertEqual(self.html.count('data-connection="pressurized"'), 2) + self.assertIn('data-link="wugang-2--greenhouse"', self.html) + self.assertIn('data-link="habitat-1--power-north"', self.html) + self.assertNotIn("perimeter-corridor", self.html) + + def test_walkways_and_radiators_are_not_clickable(self) -> None: + self.assertNotRegex( + self.html, + r'<(?:line|path|rect)[^>]+data-connection="pressurized"[^>]+data-module=', + ) + self.assertNotRegex( + self.html, + r'<(?:g|rect)[^>]+class="[^"]*radiator[^"]*"[^>]+data-module=', + ) + + def test_future_extensions_exist_only_as_source_comments(self) -> None: + comments = "\n".join(re.findall(r"", self.html, flags=re.S)) + visible_source = re.sub(r"", "", self.html, flags=re.S) + for direction in ("north", "east", "west"): + self.assertIn(f"FUTURE_EXPANSION_ANCHOR:{direction}", comments) + self.assertNotIn("未来北部十字结构", visible_source) + self.assertNotIn("未来东部十字结构", visible_source) + self.assertNotIn("未来西部十字结构", visible_source) + + def test_every_clickable_module_has_dimensions_field(self) -> None: + for module_id in CLICKABLE_MODULES: + self.assertRegex( + self.html, + rf'"{re.escape(module_id)}"\s*:\s*\{{.*?dimensions\s*:', + msg=f"{module_id} is missing dimensions", + ) + + def test_timeline_contains_only_the_two_real_corridors(self) -> None: + for incorrect in ( + "4段加压走道", + "居住舱#1↔居住舱#2", + "居住舱#2↔实验舱", + "实验舱↔温室", + "温室↔居住舱#1", + "环形加压通路", + "8个对接面", + "4段加压走道48小时", + ): + self.assertNotIn(incorrect, self.ghc05) + self.assertIn("居住舱#1远端对接口", self.ghc05) + self.assertIn("电力模块北部", self.ghc05) + self.assertIn("温室远端对接口", self.ghc05) + self.assertIn("吴刚#2", self.ghc05) + self.assertIn("从温室模块远端接口断开", self.ghc05) + + +if __name__ == "__main__": + unittest.main() +``` + +- [ ] **Step 2: Run the test and verify the expected failures** + +Run: + +```powershell +python -m unittest tests.test_guanghan_topology_demo -v +``` + +Expected: failures for missing `data-module`/`data-connection` contracts, absent future anchor comments, incomplete dimensions data, and the incorrect ring-corridor timeline text. + +- [ ] **Step 3: Commit the failing contract** + +```powershell +git add tests/test_guanghan_topology_demo.py +git commit -m "test: define Guanghan topology contract" -m "Co-Authored-By: Codex " +``` + +### Task 2: Correct the GHC-05 timeline record + +**Files:** +- Modify: `user_input/广寒计划/Timeline.md:270-296` +- Test: `tests/test_guanghan_topology_demo.py` + +- [ ] **Step 1: Rewrite the affected GHC-05 entries without changing dates or EVA numbers** + +Apply these factual replacements: + +```text +2024-12-25 EVA-7: +卸下居住舱#2、实验舱,以及用于“居住舱#1↔电力模块北部”和“温室↔吴刚#2”的两段加压走道组件。 + +2024-12-28 EVA-10: +居住舱#1安装到核心舱南侧;其远端通过加压走道连接电力模块北部。 + +2024-12-31 EVA-13: +吴刚#2通过加压走道连接温室远端对接口;对该走道执行结构锁紧与50 psi氦气检漏。 + +2025-01-01 EVA-14: +复核居住舱#1↔电力模块北部与温室↔吴刚#2两条加压走道的密封、照明和舱门联锁。 + +2025-01-02 EVA-15: +将“4段加压走道48小时密封保持确认”改为“两段加压走道48小时密封保持确认”。 + +2025-01-18 EVA-18: +将“从吴刚#2侧断开吴刚#2与广寒核心舱”改为“从吴刚#2侧断开吴刚#2与温室模块远端接口”。 +``` + +Preserve the existing mass, dimensions, power, crew, and mission-operation facts around these sentences. + +- [ ] **Step 2: Run the timeline contract** + +Run: + +```powershell +python -m unittest tests.test_guanghan_topology_demo.GuanghanTopologySourceTests.test_timeline_contains_only_the_two_real_corridors -v +``` + +Expected: `OK`. + +- [ ] **Step 3: Review the focused diff** + +Run: + +```powershell +git diff -- user_input/广寒计划/Timeline.md +``` + +Expected: only the six affected GHC-05 entries change; dates, EVA numbering, and unrelated facts remain intact. + +- [ ] **Step 4: Commit the timeline correction** + +```powershell +git add user_input/广寒计划/Timeline.md +git commit -m "docs: 修正广寒基地加压走道记录" -m "Co-Authored-By: Codex " +``` + +### Task 3: Build the semantic single-canvas topology + +**Files:** +- Modify: `data/wiki/guanghan_topology_demo.html` +- Test: `tests/test_guanghan_topology_demo.py` +- Reference: `user_input/广寒计划/Timeline.md` +- Reference: `data/wiki/guanghan_ghc01_zh.html` +- Reference: `data/wiki/guanghan_ghc03_zh.html` +- Reference: `data/wiki/guanghan_ghc05_zh.html` +- Reference: `data/wiki/wugang_lunar_lander_zh.html` +- Reference: `data/wiki/guanghan_bl01_zh.html` +- Reference: `data/wiki/guanghan_bl02_zh.html` +- Reference: `data/wiki/guanghan_bl03_zh.html` + +- [ ] **Step 1: Replace the old CSS grid with an inline SVG contract** + +Use this exact semantic pattern for every visible node and connection: + +```html + + 广寒基地 GHC-05 任务后拓扑 + 核心舱十字结构向南连接电力模块十字结构。 + + + + + + + + + + + + 广寒核心舱 + 四向安装节点 + + +``` + +Create all 14 `data-module` groups listed in the source-contract test. Position them as approved: + +- Core cross: habitat-2 north, laboratory east, habitat-1 south, greenhouse west. +- Wugang-2 beyond greenhouse on the west green corridor. +- Power cross below habitat-1: north rack/corridor, east nuclear-fuel/centrifuge, south reactor, west ISRU/mining/storage. +- Binglun-1 west, Binglun-2 south, Binglun-3 east, each on purple dashed utility lines. + +Place one non-interactive `.radiator` group above each power branch, offset away from the green north corridor. Do not put `data-module` on radiator groups. + +- [ ] **Step 2: Add responsive engineering-map styling** + +Use CSS variables and explicit line styles: + +```css +:root { + --bg: #070d18; + --panel: #0d1728; + --structural: #8fa1b8; + --pressurized: #51e89c; + --utility: #b178e6; + --core: #ffad42; + --habitat: #52baff; + --power: #d5b642; + --vehicle: #c28cdf; +} + +.map-shell { overflow: auto; border: 1px solid #26344a; border-radius: 16px; } +.topology-map { display: block; width: 100%; min-width: 980px; height: auto; background: var(--bg); } +.connection { fill: none; vector-effect: non-scaling-stroke; } +.structural { stroke: var(--structural); stroke-width: 5; } +.pressurized { stroke: var(--pressurized); stroke-width: 8; } +.utility { stroke: var(--utility); stroke-width: 4; stroke-dasharray: 12 9; } +.topology-node { cursor: pointer; outline: none; } +.topology-node:hover rect, +.topology-node:focus-visible rect { filter: brightness(1.24); stroke-width: 5; } +.radiator { pointer-events: none; } + +@media (max-width: 760px) { + body { padding: 14px; } + .topology-map { min-width: 900px; } +} +``` + +Do not apply SVG filters to zero-width/zero-height corridor lines; the approved mockup proved Chromium can clip those lines even when computed styles report a green stroke. + +- [ ] **Step 3: Add the centralized module facts** + +Define all entries with this exact shape: + +```javascript +const MODULE_DETAILS = { + "habitat-1": { + name: "居住舱#1", + type: "居住舱段", + location: "广寒核心舱南部安装口", + role: "长期驻留与乘员生活空间,提供4个折叠铺位和2个睡袋挂点。", + metrics: ["质量:12.2 t", "居住配置:4铺位 + 2睡袋"], + dimensions: ["运输:高2.7 m × 长6.2 m × 宽5.0 m", "展开:高3.2 m × 长6.2 m × 宽7.6 m"], + status: "已展开并投入使用", + connections: ["北:广寒核心舱(结构直连)", "南:电力模块北部(加压走道)"], + mission: "GHC-05 · 2024-12-28 EVA-10完成安装" + } +}; +``` + +Populate the other 13 entries from the reference pages and corrected timeline. Use `"资料未记录"` for unavailable dimensions rather than inventing values. In particular, preserve these confirmed dimensions: + +- Greenhouse transport: `高2.8 m × 长8.7 m × 宽5.0 m`; deployed: `高3.2 m × 长8.7 m × 宽7.4 m`. +- Habitat-1 transport/deployed values shown above. +- Habitat-2 deployed: `高3.2 m × 长6.2 m × 宽7.6 m`; if transport dimensions are not explicitly recorded, show `资料未记录`. +- Laboratory transport: `高2.7 m × 长7.5 m × 宽5.0 m`; deployed: `高3.2 m × 长7.5 m × 宽7.5 m`. +- Core dry mass: `28.2 t`; unavailable physical dimensions: `资料未记录`. + +- [ ] **Step 4: Run the static contract** + +Run: + +```powershell +python -m unittest tests.test_guanghan_topology_demo -v +``` + +Expected: all tests pass except no browser behavior is asserted yet. + +- [ ] **Step 5: Commit the semantic topology** + +```powershell +git add data/wiki/guanghan_topology_demo.html tests/test_guanghan_topology_demo.py +git commit -m "feat: rebuild Guanghan topology diagram" -m "Co-Authored-By: Codex " +``` + +### Task 4: Add failing browser tests for modal behavior and rendered corridors + +**Files:** +- Create: `tests/test_guanghan_topology_e2e.py` +- Modify: `data/wiki/guanghan_topology_demo.html` + +- [ ] **Step 1: Write the browser test** + +```python +from __future__ import annotations + +import asyncio +import functools +import threading +import unittest +from contextlib import contextmanager +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path + +from playwright.async_api import async_playwright + + +ROOT = Path(__file__).resolve().parents[1] + + +@contextmanager +def local_server(): + handler = functools.partial(SimpleHTTPRequestHandler, directory=str(ROOT)) + server = ThreadingHTTPServer(("127.0.0.1", 0), handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + yield f"http://127.0.0.1:{server.server_port}" + finally: + server.shutdown() + server.server_close() + thread.join(timeout=2) + + +class GuanghanTopologyE2ETests(unittest.TestCase): + def test_rendered_topology_and_modal_interactions(self) -> None: + async def scenario(base_url: str) -> None: + async with async_playwright() as pw: + browser = await pw.chromium.launch(headless=True) + page = await browser.new_page(viewport={"width": 1440, "height": 1100}) + errors: list[str] = [] + page.on("console", lambda msg: errors.append(msg.text) if msg.type == "error" else None) + await page.goto( + f"{base_url}/data/wiki/guanghan_topology_demo.html", + wait_until="networkidle", + ) + + self.assertEqual(await page.locator("[data-module]").count(), 14) + self.assertEqual(await page.locator('[data-connection="pressurized"]').count(), 2) + strokes = await page.locator('[data-connection="pressurized"]').evaluate_all( + "(els) => els.map((el) => getComputedStyle(el).stroke)" + ) + self.assertEqual(strokes, ["rgb(81, 232, 156)", "rgb(81, 232, 156)"]) + + for forbidden in ("未来北部十字结构", "未来东部十字结构", "未来西部十字结构"): + self.assertEqual(await page.get_by_text(forbidden, exact=False).count(), 0) + + await page.locator('[data-module="core"]').click() + self.assertEqual(await page.locator("#detail-modal").get_attribute("aria-hidden"), "false") + self.assertEqual(await page.locator("#detail-title").inner_text(), "广寒核心舱") + await page.keyboard.press("Escape") + self.assertEqual(await page.locator("#detail-modal").get_attribute("aria-hidden"), "true") + + await page.locator('[data-module="habitat-1"]').click() + self.assertIn("运输:高2.7 m", await page.locator("#detail-dimensions").inner_text()) + await page.locator("#detail-modal").click(position={"x": 5, "y": 5}) + self.assertEqual(await page.locator("#detail-modal").get_attribute("aria-hidden"), "true") + + self.assertEqual(errors, []) + await browser.close() + + with local_server() as base_url: + asyncio.run(scenario(base_url)) + + +if __name__ == "__main__": + unittest.main() +``` + +- [ ] **Step 2: Run the E2E test and verify it fails** + +Run: + +```powershell +python -m unittest tests.test_guanghan_topology_e2e -v +``` + +Expected: fail because the accessible modal contract or event handlers are not yet complete. + +- [ ] **Step 3: Implement the accessible modal** + +Add this modal structure after the map: + +```html + +``` + +Use these functions and handlers: + +```javascript +const modal = document.querySelector("#detail-modal"); +const dialog = modal.querySelector(".detail-dialog"); +const closeButton = modal.querySelector(".modal-close"); +let opener = null; + +function lines(items) { + return items.map((item) => `${item}`).join(""); +} + +function openDetails(moduleId, trigger) { + const detail = MODULE_DETAILS[moduleId]; + if (!detail) return; + opener = trigger; + document.querySelector("#detail-type").textContent = detail.type; + document.querySelector("#detail-title").textContent = detail.name; + document.querySelector("#detail-role").textContent = detail.role; + document.querySelector("#detail-location").textContent = detail.location; + document.querySelector("#detail-status").textContent = detail.status; + document.querySelector("#detail-metrics").innerHTML = lines(detail.metrics); + document.querySelector("#detail-dimensions").innerHTML = lines(detail.dimensions); + document.querySelector("#detail-connections").innerHTML = lines(detail.connections); + document.querySelector("#detail-mission").textContent = detail.mission; + modal.setAttribute("aria-hidden", "false"); + document.body.classList.add("modal-open"); + dialog.focus(); +} + +function closeDetails() { + modal.setAttribute("aria-hidden", "true"); + document.body.classList.remove("modal-open"); + opener?.focus(); +} + +document.querySelectorAll("[data-module]").forEach((node) => { + node.addEventListener("click", () => openDetails(node.dataset.module, node)); + node.addEventListener("keydown", (event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + openDetails(node.dataset.module, node); + } + }); +}); +closeButton.addEventListener("click", closeDetails); +modal.addEventListener("click", (event) => { + if (event.target === modal) closeDetails(); +}); +document.addEventListener("keydown", (event) => { + if (event.key === "Escape" && modal.getAttribute("aria-hidden") === "false") closeDetails(); +}); +``` + +Render detail values as text except for arrays passed through the fixed `lines()` helper; all array entries are authored local constants, not user input. + +- [ ] **Step 4: Run E2E and full source tests** + +Run: + +```powershell +python -m unittest tests.test_guanghan_topology_demo tests.test_guanghan_topology_e2e -v +``` + +Expected: all tests report `OK`, with no browser console errors. + +- [ ] **Step 5: Commit interaction behavior** + +```powershell +git add data/wiki/guanghan_topology_demo.html tests/test_guanghan_topology_e2e.py +git commit -m "feat: add Guanghan module detail modal" -m "Co-Authored-By: Codex " +``` + +### Task 5: Browser visual verification and final polish + +**Files:** +- Modify if needed: `data/wiki/guanghan_topology_demo.html` +- Verify: `user_input/广寒计划/Timeline.md` + +- [ ] **Step 1: Serve the workspace locally** + +Run from the project root: + +```powershell +python -m http.server 8765 --bind 127.0.0.1 +``` + +Expected URL: + +```text +http://127.0.0.1:8765/data/wiki/guanghan_topology_demo.html +``` + +- [ ] **Step 2: Capture and inspect a 1440×1100 desktop screenshot** + +Verify in the screenshot: + +- Core is the four-way center of habitat-2, laboratory, habitat-1, and greenhouse. +- Both green corridors are visible as actual pixels, with no inline corridor labels. +- Power north radiator does not cover the habitat-1 corridor. +- Power cross is fully expanded. +- Binglun-1/2/3 appear west/south/east. +- No future expansion label or placeholder is visible. +- The top legend appears exactly once. + +- [ ] **Step 3: Capture and inspect representative modal screenshots** + +Open and inspect: + +- Habitat-1: transport and deployed dimensions are visible. +- Power west: ISRU, mining, and ore/fuel storage appear together. +- Wugang-2: greenhouse distal-port connection is stated. + +Verify close button, backdrop close, `Esc`, and keyboard focus. + +- [ ] **Step 4: Run final verification** + +Run: + +```powershell +python -m unittest tests.test_guanghan_topology_demo tests.test_guanghan_topology_e2e -v +git diff --check +git status --short +``` + +Expected: tests `OK`; no whitespace errors; only intended task files differ from the starting worktree. + +- [ ] **Step 5: Commit any screenshot-driven polish** + +If visual inspection required code changes: + +```powershell +git add data/wiki/guanghan_topology_demo.html +git commit -m "style: polish Guanghan topology layout" -m "Co-Authored-By: Codex " +``` + +If no code changes were needed, do not create an empty commit.