Redesign an existing website to make it better. Built on top of impeccable.
71
89%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
#!/usr/bin/env node
/**
* skills/deploy/scripts/qa-gate.mjs — the stock Local-QA assertion gate (#101).
*
* Three e2e runs showed every conversion hand-rolling an ad-hoc probe.mjs
* (~3–5 min each) asserting the same invariants the section schema already
* encodes. This is that probe, bundled: point it at the HARNESS page (through
* the dev server) and the page's eds-schema JSON and it asserts the whole
* decoration contract in one run.
*
* node skills/deploy/scripts/qa-gate.mjs http://localhost:3000/qa/page.html \
* --schema stardust/eds-schema/<page>.json [--maxw 1340]
*
* Asserts (FAIL → exit 1):
* - the runtime booted: body.appear present (a blank render = harness bug, #40)
* - exactly one <h1>, no heading nested inside it (#35/#55)
* - >0 sections; every [data-block-name] reaches data-block-status="loaded"
* - zero pageerror events; zero broken images (loaded but naturalWidth 0)
* - every block instance renders non-empty (height > 5px, has child elements)
* - schema unit counts: each schema section with repeats count N≥2 renders
* ≥N units in its matching page section (grid/flex children or unit-level
* headings — the 1-of-N / 0-of-N segmentation collapse, #48/#52/#62)
* - wide-viewport (#13, second pass at 1600px): block content boxes stay
* ≤ --maxw unless the block is genuinely full-bleed in the schema order —
* over-wide boxes print as WARN (cross-check against the prototype).
* - --full-bleed a,b (the INVERSE of #13): for blocks the prototype renders
* edge-to-edge, the block's section wrapper must compute the full viewport
* width. A template-level cap (`main > .section > div { max-width }`) that
* out-specifies the block's own `max-width: none` squeezes heroes, dark
* bands and card grids into a capped column with white gutters — it reads
* as a block bug and shipped three times before the template rule was
* found. WARN with the measured widths (cross-check the template CSS).
*
* Deliberately NOT here: CLS (deployed-URL only — the harness false-passes,
* #100/#101), content/visual-diff (Step 10, deployed-URL only), and
* interactive drives (#28 — hand-write those per block).
*/
/* eslint-disable no-console, no-await-in-loop, no-restricted-syntax */
import { chromium } from 'playwright';
import fs from 'fs';
const args = process.argv.slice(2);
const url = args.find((a) => !a.startsWith('--'));
const opt = (name, dflt) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : dflt; };
const schemaPath = opt('schema', null);
const maxw = Number(opt('maxw', 1340));
const fullBleed = (opt('full-bleed', '') || '').split(',').map((s) => s.trim()).filter(Boolean);
if (!url) { console.error('usage: qa-gate.mjs <harnessURL> [--schema <eds-schema.json>] [--maxw 1340] [--full-bleed hero,band]'); process.exit(2); }
const schema = schemaPath ? JSON.parse(fs.readFileSync(schemaPath, 'utf8')) : null;
const fails = [];
const warns = [];
const ok = [];
const check = (cond, label, detail = '') => (cond ? ok : fails).push(`${label}${detail ? ` — ${detail}` : ''}`);
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 900 }, reducedMotion: 'reduce' });
const pageErrors = [];
page.on('pageerror', (e) => pageErrors.push(e.message));
await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 });
// let lazy sections load
await page.evaluate(async () => {
for (let y = 0; y < document.body.scrollHeight; y += 600) { window.scrollTo(0, y); await new Promise((r) => { setTimeout(r, 60); }); }
window.scrollTo(0, 0);
});
await page.waitForTimeout(1200);
const r = await page.evaluate(() => {
const out = {};
out.appear = document.body.classList.contains('appear');
out.h1 = document.querySelectorAll('h1').length;
out.h1Nested = document.querySelectorAll('h1 h1, h1 h2, h1 h3, h1 h4, h1 h5, h1 h6').length;
out.sections = document.querySelectorAll('main .section').length;
out.blocks = [...document.querySelectorAll('[data-block-name]')].map((b) => {
const rect = b.getBoundingClientRect();
const containers = [b, ...b.querySelectorAll('*')].filter((e) => ['grid', 'flex'].includes(getComputedStyle(e).display));
const cand = containers[0] || null;
// the repeating container is the DENSEST grid/flex, not the first (an
// outer 2-col layout wrapping a 5-item rail would otherwise read as 2)
const unitCount = containers.reduce((m, e) => Math.max(m, e.children.length), 0);
// tag-level unit proxies (the schema's unitSelector tag maps to these)
const tagCounts = {
article: b.querySelectorAll('article').length,
li: b.querySelectorAll('li').length,
figure: b.querySelectorAll('figure').length,
details: b.querySelectorAll('details').length, // accordions/FAQ units (was a false-negative "units 0")
};
const headingUnits = b.querySelectorAll('h3, h4, h5').length;
return {
name: b.dataset.blockName,
status: b.dataset.blockStatus,
height: Math.round(rect.height || b.scrollHeight),
kids: b.childElementCount,
layout: cand ? getComputedStyle(cand).display : 'block-only',
unitCount,
headingUnits,
tagCounts,
};
});
// content.da.live / admin.da.live are auth-gated → 401 to an anon harness browser,
// so they read as "broken" locally though they ingest fine (was a false-negative).
// The delivered-URL check (B1 .plain.html about:error / img-count) is the real image gate.
out.brokenImgs = [...document.querySelectorAll('img')]
.filter((i) => !/(content|admin)\.da\.live/.test(i.currentSrc || i.src || ''))
.filter((i) => i.complete && !i.naturalWidth).length;
// page sections that hold blocks, in order (for schema matching)
out.blockSections = [...document.querySelectorAll('main .section')]
.filter((s) => s.querySelector('[data-block-name]'))
.map((s) => {
const b = s.querySelector('[data-block-name]');
return { block: b.dataset.blockName };
});
return out;
});
check(r.appear, 'body.appear set (runtime booted)');
check(r.h1 === 1, 'exactly one <h1>', `found ${r.h1}`);
check(r.h1Nested === 0, 'no heading nested inside <h1>', `found ${r.h1Nested}`);
check(r.sections > 0, 'main .section count > 0', `${r.sections} sections`);
check(pageErrors.length === 0, 'zero pageerror events', pageErrors.slice(0, 3).join(' | '));
check(r.brokenImgs === 0, 'zero broken images', `${r.brokenImgs} broken`);
for (const b of r.blocks) {
check(b.status === 'loaded', `block ${b.name} loaded`, `status=${b.status}`);
check(b.height > 5 && b.kids > 0, `block ${b.name} renders non-empty`, `h=${b.height} kids=${b.kids}`);
}
// schema unit counts — match Nth block-bearing page section to Nth schema
// section that has repeats is fragile; match by ORDER over all schema sections.
if (schema && Array.isArray(schema.sections)) {
const withRepeats = schema.sections
.map((s, i) => ({ ...s, i }))
.filter((s) => (s.repeats || []).some((rep) => rep.count >= 2));
// page block instances in DOM order:
const instances = r.blocks.filter((b) => !['header', 'footer'].includes(b.name));
// schema sections (excluding chrome) in order:
const protoSections = schema.sections.filter((s) => !['header', 'footer'].includes(s.section));
withRepeats.forEach((s) => {
const pos = protoSections.indexOf(protoSections.find((x) => x.section === s.section));
const inst = instances[pos];
if (!inst) { warns.push(`schema section "${s.section}" (repeats) has no matching page block by order — verify manually`); return; }
const want = Math.max(...s.repeats.map((rep) => rep.count));
// schema-informed proxy: the unitSelector's TAG (proto classes don't survive
// conversion, the semantic tag usually does), else densest grid / headings
const tags = s.repeats.map((rep) => (rep.unitSelector || '').split('.')[0].toLowerCase()).filter(Boolean);
const tagGot = Math.max(0, ...tags.map((t) => (inst.tagCounts || {})[t] || 0));
const got = Math.max(inst.unitCount, inst.headingUnits, tagGot);
check(got >= want, `units: "${s.section}" → block ${inst.name} renders ≥${want}`, `rendered ${got} (grid kids ${inst.unitCount} / unit headings ${inst.headingUnits} / tag ${tagGot})`);
});
}
// full-bleed pass (inverse of #13): the declared full-bleed blocks' section
// wrappers must span the viewport; a template-level max-width cap out-specifying
// the block's `max-width: none` is the recorded cause.
if (fullBleed.length) {
const fb = await page.evaluate((names) => {
const vw = document.documentElement.clientWidth;
return [...document.querySelectorAll('[data-block-name]')]
.filter((b) => names.includes(b.dataset.blockName))
.map((b) => {
const wrapper = b.closest('main > .section > div') || b.parentElement;
return { name: b.dataset.blockName, w: Math.round(wrapper.getBoundingClientRect().width), vw };
});
}, fullBleed);
fullBleed.filter((n) => !fb.some((b) => b.name === n)).forEach((n) => warns.push(`full-bleed: block ${n} not found on the page — check the --full-bleed list`));
fb.forEach((b) => (b.w >= b.vw - 2 ? ok : warns).push(`full-bleed: block ${b.name} wrapper spans ${b.w}px of ${b.vw}px viewport${b.w < b.vw - 2 ? ' — a template-level `main > .section > div { max-width }` cap is likely out-specifying the block\'s escape rule; define ONE full-bleed escape at template level and route the block through it' : ''}`));
}
// wide-viewport pass (#13)
await page.setViewportSize({ width: 1600, height: 900 });
await page.waitForTimeout(600);
const wide = await page.evaluate(() => [...document.querySelectorAll('[data-block-name]')].map((b) => {
const inner = b.querySelector('.wrap, [class*="inner"], [class*="container"]') || b.firstElementChild;
return { name: b.dataset.blockName, w: inner ? Math.round(inner.getBoundingClientRect().width) : 0 };
}));
wide.filter((b) => b.w > maxw + 40 && !['header', 'footer'].includes(b.name) && !fullBleed.includes(b.name)) // declared full-bleed blocks are exempt from #13
.forEach((b) => warns.push(`wide-1600: block ${b.name} content spans ${b.w}px (> ${maxw}) — full-bleed is correct ONLY if the prototype section has no inner max-width wrapper (#13)`));
await browser.close();
ok.forEach((l) => console.log(` ✓ ${l}`));
warns.forEach((l) => console.log(` ⚠ ${l}`));
fails.forEach((l) => console.log(` ✗ ${l}`));
console.log(`QA GATE: ${fails.length === 0 ? 'PASS' : 'FAIL'} — ${ok.length} ok, ${warns.length} warn, ${fails.length} fail`);
process.exit(fails.length ? 1 : 0);.tessl-plugin
skills
audit
reference
deploy
diff
direct
extract
migrate
prepare-migration
prototype
replica
reskin
rollout
stardust
uplift
reference