Files
KSP_project/docs/superpowers/plans/2026-07-09-guanghan-topology.md
T
2026-07-09 16:43:35 +08:00

595 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <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:
```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 <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:
```html
<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:
```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 <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**
```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
<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:
```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) => `<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:
```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 <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:
```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 <noreply@openai.com>"
```
If no code changes were needed, do not create an empty commit.