367 lines
12 KiB
Markdown
367 lines
12 KiB
Markdown
# Guanghan Temporal Topology Repair 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:** Repair the approved audit findings so the Wiki topology is genuinely database-driven, historically accurate, deployable, navigable, readable, and covered by automated and screenshot verification.
|
||
|
||
**Architecture:** The Flask topology service will serve both the local demo page and same-origin JSON endpoints. PostgreSQL remains the source of truth for missions, components, connections, state, and layout; the browser will create SVG nodes and edges entirely from the snapshot response. Layout-only presentation metadata stays in layout tables, while operational facts remain in temporal topology tables.
|
||
|
||
**Tech Stack:** PostgreSQL 16, psycopg 3, Flask 3, vanilla JavaScript, SVG, Python unittest, Playwright.
|
||
|
||
---
|
||
|
||
## File map
|
||
|
||
- `topology_api/config.py`: secure environment loading and service port.
|
||
- `topology_api/app.py`: Flask app, health endpoint, and local demo-page serving.
|
||
- `topology_api/routes/snapshot.py`: JSON request validation, snapshot and mission-list endpoints.
|
||
- `topology_api/queries/snapshot_queries.py`: mission list, snapshot, component provenance, and layout queries.
|
||
- `topology_api/migrations/005_layout_presentation.sql`: layout node membership and presentation metadata.
|
||
- `topology_api/seed/seed_ghc01_to_ghc05.py`: complete mission anchors, correct temporal facts, component data.
|
||
- `topology_api/seed/seed_layout.py`: persisted node groups, concise labels, and radiator decorations.
|
||
- `data/wiki/guanghan_topology_demo.html`: same-origin API client, dynamic SVG renderer, time controls, modal, navigation.
|
||
- `tests/test_topology_api.py`: real API/database contract coverage.
|
||
- `tests/test_guanghan_topology_demo.py`: current source-level page contract.
|
||
- `tests/test_guanghan_topology_e2e.py`: browser behavior against the real Flask app and database.
|
||
|
||
### Task 1: Secure and unify local serving
|
||
|
||
**Files:**
|
||
- Modify: `topology_api/config.py`
|
||
- Modify: `topology_api/app.py`
|
||
- Create: `tests/test_topology_api.py`
|
||
- Modify: `data/wiki/guanghan_topology_demo.html`
|
||
|
||
- [ ] **Step 1: Write failing tests**
|
||
|
||
Add tests asserting:
|
||
|
||
```python
|
||
def test_app_serves_demo_page_and_health(client):
|
||
assert client.get("/health").status_code == 200
|
||
page = client.get("/guanghan_topology_demo.html")
|
||
assert page.status_code == 200
|
||
assert "广寒基地拓扑结构" in page.get_data(as_text=True)
|
||
|
||
def test_page_uses_same_origin_api():
|
||
html = HTML_PATH.read_text(encoding="utf-8")
|
||
assert "http://localhost:5000" not in html
|
||
assert "const API_BASE = ''" in html
|
||
|
||
def test_config_has_no_default_password():
|
||
source = CONFIG_PATH.read_text(encoding="utf-8")
|
||
assert 'os.environ.get("PGPASSWORD",' not in source
|
||
```
|
||
|
||
- [ ] **Step 2: Run tests and verify RED**
|
||
|
||
Run:
|
||
|
||
```text
|
||
python -m unittest tests.test_topology_api -v
|
||
```
|
||
|
||
Expected failures: no demo route, hardcoded API host, and hardcoded password.
|
||
|
||
- [ ] **Step 3: Implement minimal serving and configuration**
|
||
|
||
Use `load_dotenv(PROJECT_ROOT / ".env")`, require `PGHOST`, `PGPORT`, `PGUSER`, and `PGPASSWORD`, default only the non-secret topology database name, and default the topology service port to `8080`.
|
||
|
||
Serve the existing page:
|
||
|
||
```python
|
||
@app.get("/guanghan_topology_demo.html")
|
||
def topology_demo():
|
||
return send_from_directory(WIKI_DIR, "guanghan_topology_demo.html")
|
||
```
|
||
|
||
Use `const API_BASE = ''` so browser requests remain same-origin.
|
||
|
||
- [ ] **Step 4: Run tests and verify GREEN**
|
||
|
||
Run the Task 1 tests and confirm zero failures.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```text
|
||
git commit -m "fix(topology): secure and unify demo serving"
|
||
```
|
||
|
||
### Task 2: Validate API requests and expose missions
|
||
|
||
**Files:**
|
||
- Modify: `topology_api/routes/snapshot.py`
|
||
- Modify: `topology_api/queries/snapshot_queries.py`
|
||
- Modify: `tests/test_topology_api.py`
|
||
|
||
- [ ] **Step 1: Write failing API tests**
|
||
|
||
Cover:
|
||
|
||
```python
|
||
def test_invalid_time_is_json_400(client):
|
||
response = client.get("/api/v1/bases/guanghan/snapshot?at=not-a-time")
|
||
assert response.status_code == 400
|
||
assert response.json["error"] == "invalid_time"
|
||
|
||
def test_time_and_mission_preserve_context(client):
|
||
response = client.get(
|
||
"/api/v1/bases/guanghan/snapshot"
|
||
"?mission=GHC-01&at=2025-01-02T08:00:00%2B08:00"
|
||
)
|
||
assert response.status_code == 200
|
||
assert response.json["query"]["requestedMission"] == "GHC-01"
|
||
assert response.json["query"]["anchorSource"] == "explicit_time"
|
||
|
||
def test_mission_list_is_database_backed(client):
|
||
response = client.get("/api/v1/bases/guanghan/missions")
|
||
assert response.status_code == 200
|
||
assert "GHC-05" in [row["code"] for row in response.json["missions"]]
|
||
```
|
||
|
||
- [ ] **Step 2: Run tests and verify RED**
|
||
|
||
Expected failures: invalid time is 500 and the mission endpoint is 404.
|
||
|
||
- [ ] **Step 3: Implement validation and mission query**
|
||
|
||
Parse ISO 8601 with `datetime.fromisoformat(value.replace("Z", "+00:00"))`, require a timezone, normalize to ISO output, and return JSON 400 on failure.
|
||
|
||
Add `fetch_missions(base_code)` returning code, title, occurrence status, and maximum event time ordered chronologically.
|
||
|
||
- [ ] **Step 4: Run tests and verify GREEN**
|
||
|
||
Run `python -m unittest tests.test_topology_api -v`.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```text
|
||
git commit -m "fix(topology): validate time and list missions"
|
||
```
|
||
|
||
### Task 3: Correct temporal seed facts
|
||
|
||
**Files:**
|
||
- Modify: `topology_api/seed/seed_ghc01_to_ghc05.py`
|
||
- Modify: `tests/test_topology_api.py`
|
||
|
||
- [ ] **Step 1: Write failing mission-anchor and port tests**
|
||
|
||
Assert all nine expected terminal instants:
|
||
|
||
```python
|
||
EXPECTED_LAST_EVENTS = {
|
||
"BL-01": "2024-05-17T23:01:00+00:00",
|
||
"GHC-01": "2024-06-16T07:23:00+00:00",
|
||
"JC-01": "2024-06-17T08:19:00+00:00",
|
||
"BL-02": "2024-06-23T05:23:00+00:00",
|
||
"GHC-02": "2024-07-15T03:16:00+00:00",
|
||
"GHC-03": "2024-08-30T12:45:00+00:00",
|
||
"BL-03": "2024-09-23T19:20:00+00:00",
|
||
"GHC-04": "2024-11-20T13:57:00+00:00",
|
||
"GHC-05": "2025-01-22T10:52:00+00:00",
|
||
}
|
||
```
|
||
|
||
Also assert Binglun 1/2/3 connect to west/south/east hub ports respectively.
|
||
|
||
- [ ] **Step 2: Run and verify RED**
|
||
|
||
Expected: five terminal-time mismatches and two Binglun port mismatches.
|
||
|
||
- [ ] **Step 3: Add terminal checkpoint events and correct ports**
|
||
|
||
Add a non-topology `mission_checkpoint` event at each missing terminal instant. Map Binglun connections:
|
||
|
||
```python
|
||
hub_ports = {
|
||
"binglun_1": "west",
|
||
"binglun_2": "south",
|
||
"binglun_3": "east",
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Re-seed and verify GREEN**
|
||
|
||
Run:
|
||
|
||
```text
|
||
python -m topology_api.seed.seed_ghc01_to_ghc05
|
||
python -m topology_api.seed.seed_layout
|
||
python -m unittest tests.test_topology_api -v
|
||
```
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```text
|
||
git commit -m "fix(topology): correct mission anchors and rover ports"
|
||
```
|
||
|
||
### Task 4: Persist complete diagram semantics
|
||
|
||
**Files:**
|
||
- Create: `topology_api/migrations/005_layout_presentation.sql`
|
||
- Modify: `topology_api/seed/seed_layout.py`
|
||
- Modify: `topology_api/queries/snapshot_queries.py`
|
||
- Modify: `tests/test_topology_api.py`
|
||
|
||
- [ ] **Step 1: Write failing layout tests**
|
||
|
||
Require every layout node to expose:
|
||
|
||
```python
|
||
{
|
||
"nodeKey": "...",
|
||
"memberKeys": ["..."],
|
||
"displayMeta": "...",
|
||
"displayHint": "...",
|
||
"decorations": {"radiator": True}
|
||
}
|
||
```
|
||
|
||
Assert west and east group membership and exactly four radiator nodes.
|
||
|
||
- [ ] **Step 2: Run and verify RED**
|
||
|
||
Expected: missing layout fields.
|
||
|
||
- [ ] **Step 3: Add schema and query support**
|
||
|
||
Add `display_meta text`, `display_hint text`, and `decorations jsonb` to `diagram_nodes`, plus:
|
||
|
||
```sql
|
||
CREATE TABLE topology.diagram_node_members (
|
||
diagram_node_id uuid NOT NULL REFERENCES topology.diagram_nodes(id) ON DELETE CASCADE,
|
||
component_id uuid NOT NULL REFERENCES topology.components(id),
|
||
display_order integer NOT NULL DEFAULT 0,
|
||
PRIMARY KEY (diagram_node_id, component_id)
|
||
);
|
||
```
|
||
|
||
Seed direct and grouped members, concise approved labels, and radiator decorations.
|
||
|
||
- [ ] **Step 4: Apply migration, seed, and verify GREEN**
|
||
|
||
Execute migration transactionally with psycopg, seed layout, then run API tests.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```text
|
||
git commit -m "feat(topology): persist complete diagram semantics"
|
||
```
|
||
|
||
### Task 5: Render SVG entirely from snapshot data
|
||
|
||
**Files:**
|
||
- Replace rendering section: `data/wiki/guanghan_topology_demo.html`
|
||
- Modify: `tests/test_guanghan_topology_demo.py`
|
||
- Modify: `tests/test_guanghan_topology_e2e.py`
|
||
|
||
- [ ] **Step 1: Write failing renderer contracts**
|
||
|
||
Assert the source no longer contains hardcoded topology nodes or fixed connection inventory, uses `currentData.layout`, renders connections from `currentData.connections`, renders radiators only from visible node decorations, and retains future expansion comments.
|
||
|
||
Add E2E assertions:
|
||
|
||
- BL-01 has one node, zero connection lines, zero radiators;
|
||
- GHC-01 has two nodes and zero connection lines;
|
||
- GHC-05 has 14 visual nodes, two green pressurized lines, four radiators;
|
||
- no visible SVG text extends outside its owning node rectangle.
|
||
|
||
- [ ] **Step 2: Run and verify RED**
|
||
|
||
Expected: fixed SVG and historical visibility failures.
|
||
|
||
- [ ] **Step 3: Implement dynamic renderer**
|
||
|
||
Create SVG elements with `document.createElementNS`, map component keys to persisted layout nodes, aggregate multiple database edges that share visual endpoints and line class, calculate rectangle-edge intersections from node centers, and attach click/keyboard handlers after node creation.
|
||
|
||
Use concise persisted `displayMeta`/`displayHint`; truncate text based on node width and add a `<title>` for full content. Render a radiator only when the visible layout node declares `decorations.radiator`.
|
||
|
||
- [ ] **Step 4: Run and verify GREEN**
|
||
|
||
Run source and browser tests.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```text
|
||
git commit -m "feat(topology): render temporal graph from database"
|
||
```
|
||
|
||
### Task 6: Repair controls, navigation, errors, and modal semantics
|
||
|
||
**Files:**
|
||
- Modify: `topology_api/queries/snapshot_queries.py`
|
||
- Modify: `data/wiki/guanghan_topology_demo.html`
|
||
- Modify: `tests/test_topology_api.py`
|
||
- Modify: `tests/test_guanghan_topology_e2e.py`
|
||
|
||
- [ ] **Step 1: Write failing behavior tests**
|
||
|
||
Cover:
|
||
|
||
- mission dropdown comes from `/missions`;
|
||
- mission plus explicit time sends both query parameters and retains mission subtitle;
|
||
- invalid time shows a localized validation error without a request;
|
||
- browser back reloads the URL-selected snapshot;
|
||
- modal “任务节点” displays introduction mission and time;
|
||
- modal close restores focus to the triggering node;
|
||
- internal anchor enum values are localized;
|
||
- database strings are inserted with `textContent`, not `innerHTML`.
|
||
|
||
- [ ] **Step 2: Run and verify RED**
|
||
|
||
Expected: each approved audit defect reproduces.
|
||
|
||
- [ ] **Step 3: Implement minimal behavior**
|
||
|
||
Return `introducedMission` and `introducedAt` for components. Fetch mission options from the API. Always send both non-empty parameters. Add `popstate`, ISO validation, localized anchor labels, stable Asia/Shanghai formatting, safe line rendering helpers, and trigger-focus restoration.
|
||
|
||
- [ ] **Step 4: Run and verify GREEN**
|
||
|
||
Run API, source, and E2E tests.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```text
|
||
git commit -m "fix(topology): repair controls navigation and details"
|
||
```
|
||
|
||
### Task 7: Full service and screenshot verification
|
||
|
||
**Files:**
|
||
- Update only if a verified defect remains.
|
||
- Create screenshots under `demo/topology_repair/`.
|
||
|
||
- [ ] **Step 1: Run the complete automated suite**
|
||
|
||
```text
|
||
python -m unittest tests.test_topology_api tests.test_guanghan_topology_demo tests.test_guanghan_topology_e2e -v
|
||
```
|
||
|
||
Expected: all pass with no browser console errors.
|
||
|
||
- [ ] **Step 2: Restart the topology service**
|
||
|
||
Stop only the verified processes listening on ports 5000 and 8080, then start:
|
||
|
||
```text
|
||
python topology_api/app.py
|
||
```
|
||
|
||
with a hidden window and working directory at the repository root.
|
||
|
||
- [ ] **Step 3: Test and screenshot every mission**
|
||
|
||
Visit each of the nine tasks through the page control, verify node/edge/radiator counts, open representative modal details, test explicit time, invalid time, back navigation, and 390×844 layout.
|
||
|
||
- [ ] **Step 4: Review diffs and requirements**
|
||
|
||
Confirm no unrelated files changed, no secret remains in tracked source, all approved audit items have a test or explicit evidence, and the original GHC-05 spatial skeleton remains intact.
|
||
|
||
- [ ] **Step 5: Commit verification evidence**
|
||
|
||
```text
|
||
git commit -m "test(topology): verify repaired temporal topology"
|
||
```
|