22 KiB
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
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:
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
git add tests/test_guanghan_topology_demo.py
git commit -m "test: define Guanghan topology contract" -m "Co-Authored-By: Codex <noreply@openai.com>"
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:
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:
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:
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
git add user_input/广寒计划/Timeline.md
git commit -m "docs: 修正广寒基地加压走道记录" -m "Co-Authored-By: Codex <noreply@openai.com>"
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:
<svg class="topology-map" viewBox="0 0 1500 1700" role="img"
aria-labelledby="topology-title topology-desc">
<title id="topology-title">广寒基地 GHC-05 任务后拓扑</title>
<desc id="topology-desc">核心舱十字结构向南连接电力模块十字结构。</desc>
<!-- FUTURE_EXPANSION_ANCHOR:north
Reserve canvas above habitat-2 for a future cross node and multiple Wugang ports. -->
<!-- FUTURE_EXPANSION_ANCHOR:east
Reserve canvas east of laboratory for a future cross node. -->
<!-- FUTURE_EXPANSION_ANCHOR:west
Reserve canvas west of greenhouse for a future cross node and rover ports. -->
<line class="connection structural" data-connection="structural"
x1="750" y1="350" x2="750" y2="460"/>
<line class="connection pressurized" data-connection="pressurized"
data-link="wugang-2--greenhouse"
x1="220" y1="590" x2="350" y2="590"/>
<line class="connection pressurized" data-connection="pressurized"
data-link="habitat-1--power-north"
x1="750" y1="800" x2="750" y2="940"/>
<g class="topology-node module-node core-node" data-module="core"
role="button" tabindex="0" aria-label="查看广寒核心舱详情">
<rect x="630" y="480" width="240" height="220" rx="18"/>
<text x="750" y="565">广寒核心舱</text>
<text class="node-meta" x="750" y="605">四向安装节点</text>
</g>
</svg>
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:
: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:
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:
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
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 <noreply@openai.com>"
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
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:
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:
<div class="modal-backdrop" id="detail-modal" aria-hidden="true">
<section class="detail-dialog" role="dialog" aria-modal="true"
aria-labelledby="detail-title" tabindex="-1">
<button class="modal-close" type="button" aria-label="关闭详情">×</button>
<p class="detail-kicker" id="detail-type"></p>
<h2 id="detail-title"></h2>
<p id="detail-role"></p>
<dl class="detail-grid">
<div><dt>安装位置</dt><dd id="detail-location"></dd></div>
<div><dt>运行状态</dt><dd id="detail-status"></dd></div>
<div><dt>关键参数</dt><dd id="detail-metrics"></dd></div>
<div><dt>尺寸</dt><dd id="detail-dimensions"></dd></div>
<div><dt>连接关系</dt><dd id="detail-connections"></dd></div>
<div><dt>任务节点</dt><dd id="detail-mission"></dd></div>
</dl>
</section>
</div>
Use these functions and handlers:
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) => `<span>${item}</span>`).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:
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
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 <noreply@openai.com>"
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:
python -m http.server 8765 --bind 127.0.0.1
Expected URL:
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:
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:
git add data/wiki/guanghan_topology_demo.html
git commit -m "style: polish Guanghan topology layout" -m "Co-Authored-By: Codex <noreply@openai.com>"
If no code changes were needed, do not create an empty commit.