新增 oxlint@1.80.0 + .oxlintrc.json + lint/lint:fix 脚本;清空所有 no-unused-vars 告警(未用 React/导入/变量/参数),删除无引用的 ui/Skill。 oxlint exit 0。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
opencli — openapi-website A/B diff toolkit
Verification toolkit for the VitePress → Astro migration. Provides 5 layers of UX-equivalence checks between the old VitePress site (A) and the new Astro site (B).
Quick-start
# 1. Parse sitemaps and diff URL sets
bun run scripts/opencli/crawl-routes.ts dist/old/sitemap.xml --pretty > routes-old.json
bun run scripts/opencli/crawl-routes.ts dist/new/sitemap-index.xml --pretty > routes-new.json
bun run scripts/opencli/url-diff.ts dist/old/sitemap.xml dist/new/sitemap-index.xml
# 2. Snapshot HTML (standalone fetch mode — screenshots require MCP agent)
bun run scripts/opencli/snapshot.ts http://localhost:4321 routes-new.json --out-dir dist-diff/snapshots/new
bun run scripts/opencli/snapshot.ts http://localhost:5173 routes-old.json --out-dir dist-diff/snapshots/old
# 3. DOM structural diff for a single route
bun run scripts/opencli/dom-diff.ts dist-diff/snapshots/old/index.html dist-diff/snapshots/new/index.html
# 4. Visual pixel diff (requires screenshots captured by MCP agent)
bun run scripts/opencli/visual-diff.ts dist-diff/snapshots/old/index.png dist-diff/snapshots/new/index.png --diff dist-diff/visual/index.diff.png
# 5. Aggregate report
bun run scripts/opencli/report.ts --results-dir dist-diff/results --out-dir dist-diff
Architecture — 5 Layers
Layer 1: URL set equivalence (url-diff.ts)
Hard gate for Task 18. The Astro site must serve exactly the same URL paths as the VitePress site; no additions, no removals.
- Parses both sitemaps via
crawl-routes.ts(parseSitemap) - Strips origin; compares path-only to avoid false negatives from different hosts
- Exit 0 if A △ B = ∅ (symmetric difference is empty)
- Exit 1 if any mismatch; prints
onlyInOldandonlyInNewlists
Handles both flat sitemap.xml and Astro's sitemap-index.xml (sitemapindex) formats.
Layer 2: HTML capture (snapshot.ts)
Fetches the raw HTML for each URL and writes it to a per-slug file.
- Standalone mode:
fetch()for HTML only (no screenshots) - MCP-driven mode: agent uses
mcp__chrome-devtools__navigate_page+evaluate_scriptto capture live-rendered HTML andtake_screenshotfor PNGs - Output:
<outDir>/<slug>.html,manifest.json
See §"MCP-Driven Canonical Flow" below for the authoritative agent script.
Layer 3: DOM structural diff (dom-diff.ts)
Compares page structure using node-html-parser (pure JS, Bun-compatible).
Normalizes before comparison:
- Strips
data-astro-*attributes (build-time Astro annotations) - Removes Astro hash class suffixes (
astro-XXXXXX)
Metrics (weighted composite score, threshold 0.95):
| Metric | Weight | Method |
|---|---|---|
Heading sequence (h1–h6) |
40% | LCS-based sequence similarity |
| Internal link set | 30% | Jaccard similarity |
| Code block count | 15% | ratio min/max |
data-lbus-component values |
15% | Jaccard similarity |
Layer 4: Visual pixel diff (visual-diff.ts)
Wraps odiff binary from the odiff-bin devDependency.
- Default threshold: 0.1% pixel difference
- Writes diff overlay PNG for any failing pair
- Resolves binary via
createRequire('odiff-bin/package.json'), falls back toPATH
Screenshots must be captured at the same viewport (1280 × 800 recommended) by the MCP agent before running this script.
Layer 5: Interaction assertions (interaction-assertions.ts)
Reference implementation documenting 6 UI checks. The T18 MCP agent executes these
using mcp__chrome-devtools__* tools and passes results to the exported functions.
| Check | What is verified |
|---|---|
assertThemeToggle |
Clicking theme button changes dataset.mode |
assertLanguageSwitcher |
Switching locale navigates to correct path prefix |
assertSearchDialog |
Cmd+K opens dialog; typing returns results |
assertSidebarCollapse |
Collapse button hides sidebar nav |
assertCodeCopy |
Copy button writes code content to clipboard |
assertComponentPresence |
data-lbus-component values present on page |
MCP-Driven Canonical Flow
The T18 agent executes the following steps for each route using the
mcp__chrome-devtools__* tools. This is the authoritative flow; the standalone
Bun scripts in this directory are reference implementations only.
# Per-route agent script (pseudocode — T18 agent executes this inline)
for each url in routes:
slug = urlToSlug(url)
# --- Capture OLD (VitePress) ---
mcp__chrome-devtools__navigate_page({ type: "url", url: OLD_BASE + path })
mcp__chrome-devtools__take_screenshot({ filePath: `dist-diff/old/${slug}.png` })
oldHtml = mcp__chrome-devtools__evaluate_script({
function: "() => document.documentElement.outerHTML"
})
write(oldHtml, `dist-diff/old/${slug}.html`)
# --- Capture NEW (Astro) ---
mcp__chrome-devtools__navigate_page({ type: "url", url: NEW_BASE + path })
mcp__chrome-devtools__take_screenshot({ filePath: `dist-diff/new/${slug}.png` })
newHtml = mcp__chrome-devtools__evaluate_script({
function: "() => document.documentElement.outerHTML"
})
write(newHtml, `dist-diff/new/${slug}.html`)
# --- DOM diff ---
domResult = domSimilarity(oldHtml, newHtml) # from dom-diff.ts
write(domResult, `dist-diff/results/${slug}/dom-diff.json`)
# --- Visual diff ---
vizResult = visualDiff(
`dist-diff/old/${slug}.png`,
`dist-diff/new/${slug}.png`,
`dist-diff/visual/${slug}.diff.png`
)
write(vizResult, `dist-diff/results/${slug}/visual-diff.json`)
# --- Interaction assertions ---
# Theme toggle:
mcp__chrome-devtools__navigate_page({ type: "url", url: NEW_BASE + path })
initialMode = mcp__chrome-devtools__evaluate_script({
function: "() => document.documentElement.dataset.mode"
})
themeToggleUid = /* uid from take_snapshot() */
mcp__chrome-devtools__click({ uid: themeToggleUid })
afterMode = mcp__chrome-devtools__evaluate_script({
function: "() => document.documentElement.dataset.mode"
})
themeResult = assertThemeToggle(initialMode, afterMode)
# Search dialog:
mcp__chrome-devtools__press_key({ key: "Meta+k" })
snapshot = mcp__chrome-devtools__take_snapshot()
dialogVisible = snapshot.includes('[role=dialog]')
inputUid = /* uid from snapshot */
mcp__chrome-devtools__fill({ uid: inputUid, value: "quote" })
snapshot2 = mcp__chrome-devtools__take_snapshot()
resultCount = snapshot2.count('[role=option]')
searchResult = assertSearchDialog(dialogVisible, resultCount)
# ... (sidebar, code copy, component presence analogously)
interactions = [themeResult, searchResult, ...]
write(interactions, `dist-diff/results/${slug}/interactions.json`)
# --- Aggregate report ---
bun run scripts/opencli/report.ts
Script reference
| Script | Exports | CLI |
|---|---|---|
crawl-routes.ts |
parseSitemap, categorize, buildRoutes, Route |
<sitemap.xml> [--pretty] |
url-diff.ts |
diffUrls, UrlDiffResult |
<old.xml> <new.xml> [--json] |
snapshot.ts |
snapshotRoutes, captureHtml, urlToSlug |
<baseUrl> <routes.json> [--out-dir] |
dom-diff.ts |
domSimilarity, DomDiffResult, DomDiffIssue |
<old.html> <new.html> [--threshold] [--json] |
visual-diff.ts |
visualDiff, VisualDiffResult |
<old.png> <new.png> [--diff] [--threshold] [--json] |
interaction-assertions.ts |
assert* functions, InteractionResult |
--help |
report.ts |
writeReport, loadResultsDir, FullReport |
[--results-dir] [--url-diff] [--out-dir] |
Output directory layout
dist-diff/
old/ # VitePress snapshots (HTML + PNG)
index.html
index.png
manifest.json
new/ # Astro snapshots (HTML + PNG)
index.html
index.png
manifest.json
visual/ # Diff overlay PNGs
index.diff.png
results/ # Per-slug JSON results
index/
dom-diff.json
visual-diff.json
interactions.json
url-diff.json # URL diff result (from url-diff.ts --json)
report.md # Human-readable markdown report
report.json # Machine-readable full report
Pass criteria (T18 hard gates)
url-diff.tsexits 0 — A △ B = ∅- All routes:
domSimilarityscore ≥ 0.95 - All routes:
visualDiffchanged ≤ 0.1% - All routes: all 6 interaction assertions pass