diff --git a/.agents/scripts/rendered-text-diff.mjs b/.agents/scripts/rendered-text-diff.mjs new file mode 100644 index 0000000..1d5a88e --- /dev/null +++ b/.agents/scripts/rendered-text-diff.mjs @@ -0,0 +1,92 @@ +#!/usr/bin/env node +// Compare the *rendered* text of a legacy page against its Astro replacement. +// +// node .agents/scripts/rendered-text-diff.mjs full-guide +// +// Why this exists: scripts/verify.mjs reads the legacy files, so a migrated +// page can drop half its content and still pass the gate. Task 15d shipped +// /full-guide/ missing 86 rendered spans -- the entire verification section, +// the hands-on exercise brief, and both "Clone from Gitea" links -- and every +// check was green. +// +// Static HTML comparison is useless here: the guide's tab panels are injected +// by an island at runtime, so half the legacy page's markup has no static +// counterpart. This walks the live DOM instead and skips anything the browser +// is not painting -- which also drops the hidden Portuguese half of each +// bilingual pair, so the two sides line up. +// +// Requires playwright (devDependency) and two static servers; it starts both. +import { spawn } from 'node:child_process'; +import { cpSync, mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { chromium } from 'playwright'; + +const route = process.argv[2]; +if (!route) { + console.error('usage: rendered-text-diff.mjs e.g. full-guide'); + process.exit(2); +} + +// The built site expects to be served under the configured base path. +const staging = mkdtempSync(join(tmpdir(), 'af-rtd-')); +cpSync('dist', join(staging, 'ai-for-dummies'), { recursive: true }); + +const serve = (dir, port) => + spawn('python3', ['-m', 'http.server', String(port), '-d', dir], { stdio: 'ignore' }); +const servers = [serve('.', 4197), serve(staging, 4196)]; +const stop = () => { + servers.forEach((s) => s.kill()); + rmSync(staging, { recursive: true, force: true }); +}; + +// Visible text nodes, in document order, whitespace collapsed. +const visibleText = () => { + const out = []; + const walk = (node) => { + for (const child of node.childNodes) { + if (child.nodeType === Node.TEXT_NODE) { + const text = child.textContent.replace(/\s+/g, ' ').trim(); + if (text) out.push(text); + continue; + } + if (child.nodeType !== Node.ELEMENT_NODE) continue; + if (child.tagName === 'SCRIPT' || child.tagName === 'STYLE') continue; + const style = getComputedStyle(child); + if (child.hidden || style.display === 'none' || style.visibility === 'hidden') continue; + walk(child); + } + }; + walk(document.body); + return out; +}; + +try { + await new Promise((r) => setTimeout(r, 1500)); + const browser = await chromium.launch(); + const grab = async (url) => { + const page = await browser.newPage({ viewport: { width: 1400, height: 1000 } }); + await page.goto(url, { waitUntil: 'load' }); + // The islands hydrate and render their initial panel on load; without this + // every panel's copy reads as missing. + await page.waitForTimeout(1200); + const spans = await page.evaluate(visibleText); + await page.close(); + return spans; + }; + + const legacy = await grab(`http://localhost:4197/${route}/index.html`); + const astro = await grab(`http://localhost:4196/ai-for-dummies/${route}/`); + await browser.close(); + + const rendered = new Set(astro); + const missing = legacy.filter((span) => !rendered.has(span)); + + console.log( + `legacy ${legacy.length} spans · astro ${astro.length} spans · missing ${missing.length}`, + ); + for (const span of missing) console.log(` - ${span}`); + process.exitCode = missing.length === 0 ? 0 : 1; +} finally { + stop(); +} diff --git a/src/pages/full-guide.astro b/src/pages/full-guide.astro index e133917..6c43689 100644 --- a/src/pages/full-guide.astro +++ b/src/pages/full-guide.astro @@ -1064,6 +1064,22 @@ const base = import.meta.env.BASE_URL; >

+ +
+ +

+ Prefer a focused chapter? Start with the route map, then + jump directly to models, agents and worktrees, skill creation, rules, or + the skills review desk. +

+