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

22 KiB
Raw Permalink Blame History

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.