Files
longbridge__developers/scripts/opencli/README.md
twosugar 53666718af feat(migrate): opencli — url/dom/visual/interaction diff toolkit
- crawl-routes.ts: parse sitemap XML (flat + sitemapindex) → Route[]
- url-diff.ts: symmetric set diff (A △ B = ∅) hard gate for T18
- snapshot.ts: per-URL HTML capture (fetch mode); MCP screenshots documented
- dom-diff.ts: heading/links/codeblocks/components Jaccard ≥ 0.95
- visual-diff.ts: odiff-bin wrapper, 0.1% pixel threshold
- interaction-assertions.ts: theme/search/sidebar/copy/component checks (MCP ref)
- report.ts: aggregate results → dist-diff/report.md + report.json
- README.md: 5 layers + MCP-driven canonical flow

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-17 19:46:49 +08:00

218 lines
8.2 KiB
Markdown
Raw Permalink 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.
# 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
```bash
# 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 `onlyInOld` and `onlyInNew` lists
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_script`
to capture live-rendered HTML and `take_screenshot` for 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 to `PATH`
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)
1. `url-diff.ts` exits 0 — A △ B = ∅
2. All routes: `domSimilarity` score ≥ 0.95
3. All routes: `visualDiff` changed ≤ 0.1%
4. All routes: all 6 interaction assertions pass