---
## Code Review: `reports/phase3/scripts/build_top200_v2_h5.py`
This diff is a major refactoring of the H5 page builder — from a standalone inline HTML generator (~925 lines) to a template-based approach (~312 lines) that reuses `skill-market-h5.html` as a shell. The table-based layout is replaced with a card grid, the scoring scale changes from 20 to 5, and the UI switches from select-based filters to chip-based filters.
---
### `build_top200_v2_h5.py:100` — `CLEANUP` — Dead return value in `extract_h5_shell()`
**Summary:** The function returns a tuple `(head_and_body, "")` where the second element is always an empty string.
**Suggestion:** Return just `head_and_body` as a plain string, and update the caller (`shell = extract_h5_shell()` instead of `shell, _ = extract_h5_shell()`).
---
### `build_top200_v2_h5.py:105–158` — `CLEANUP` — Fragile template text replacement
**Summary:** The function performs 10+ sequential `.replace()` calls against exact HTML markup in `skill-market-h5.html`. If the template is ever reformatted or tweaked (whitespace changes, attribute reordering), these replacements silently no-op — the old strings remain in the output.
**Failure scenario:** Someone edits `skill-market-h5.html` (e.g., adds a newline inside a `
` or changes `class="sub"` to `class="subtitle"`). The build script produces output with stale V1 text labels instead of V2 labels, with no warning.
**Suggestion:** Consider one of:
- Extract translatable strings/labels into a JSON config that both the template and this script reference.
- Use regex or a small HTML parser (e.g., `html.parser`) to find elements by class or ID and replace text content only.
- At minimum, add a post-build assertion that checks the output for expected V2 strings.
---
### `build_top200_v2_h5.py:247` — `CLEANUP` — `esc()` + inline `onclick` bypasses HTML escaping in JS context
**Summary:** The `esc()` function escapes `'` to `'`, but when interpolated into an inline `onclick` attribute:
```javascript
onclick="openDrawer('${esc(r.skill_id)}')"
```
the HTML parser decodes `'` back to `'` **before** the JavaScript engine runs. If a `skill_id` contained a single quote (e.g., `it's_a_test`), the decoded JS would be:
```javascript
openDrawer('it's_a_test')
```
which is a syntax error — the string terminates at the second `'`.
**Failure scenario:** A skill_id containing `'` would break the card's click handler. Extremely unlikely given skill ID naming conventions (slugs like `namespace__skill-name`), but incorrect in principle.
**Suggestion:** Prefer event delegation — attach one click handler on the grid container and read `data-id` from the card:
```javascript
document.getElementById('grid').addEventListener('click', e => {
const card = e.target.closest('[data-id]');
if (card) openDrawer(card.dataset.id);
});
```
Same issue applies to line 268 (`onclick="copyText('${esc(r.copied_path || '')}', this)"`).
---
### `build_top200_v2_h5.py:176–182` — `CLEANUP` — `securityLabel()` fallback ordering is fragile
**Summary:** The function first checks exact keys in the `m` map, then falls back to `String(s).includes('passed')` before `String(s).includes('caution')`. If a new security value containing **both** "passed" and "caution" were introduced (e.g., `passed_with_caution_but_flagged`), it would match `includes('passed')` first and return `'通过'` instead of `'谨慎通过'`.
**Failure scenario:** A hypothetical new security status `passed_with_caution_but_flagged` would display as "通过" rather than "谨慎通过". In practice, all known values are explicitly mapped, so this only matters if new values appear at runtime without updating the code.
**Suggestion:** Check `caution` before `passed` in the fallback chain, since "caution" is a more specific qualifier:
```javascript
if (String(s).includes('caution')) return '谨慎通过';
if (String(s).includes('passed')) return '通过';
```
---
### `build_top200_v2_h5.py:233–237` — `CLEANUP` — Sort options unreachable via UI
**Summary:** The JS sort handler supports `rank`, `rank-desc`, `score`, `score-asc`, and `name` modes. But the template's `