skills/build-verification-expert/SKILL.md
Verifies build output integrity after code changes. Runs builds, validates artifact structure, checks for regressions in bundle size, asset references, and static export completeness. The last line of defense before a bad deploy ships.
npx skillsauth add curiositech/windags-skills build-verification-expertInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Systematic verification of build output after code changes. Catches the class of bugs that pass all tests but break in production: missing assets, broken static exports, inflated bundles, dead routes, and malformed sitemaps.
Use for:
Do NOT use for:
test-automation-expert)performance-profiling)github-actions-pipeline-builder or cloudflare-pages-cicd)cost-optimizer or framework-specific skills)Run the build and classify every diagnostic:
# Capture full build output with timing
time npm run build 2>&1 | tee /tmp/build-output.log
# Separate errors from warnings
grep -E "^(Error|error|ERROR)" /tmp/build-output.log > /tmp/build-errors.log
grep -iE "(warn|warning)" /tmp/build-output.log > /tmp/build-warnings.log
Error classification:
| Severity | Pattern | Action |
|----------|---------|--------|
| Fatal | Error: Build failed, non-zero exit code | Stop. Fix before proceeding. |
| Type error | TS\d+:, Type '...' is not assignable | Fix. These are bugs hiding as warnings. |
| Missing module | Module not found, Cannot resolve | Check imports, package.json, and tsconfig paths. |
| Deprecation | DeprecationWarning | Log for future cleanup, not blocking. |
| Size warning | exceeds the recommended size limit | Investigate if new, acceptable if known. |
Critical rule: A build that exits 0 but emits TypeScript errors to stderr is NOT a passing build. Many frameworks continue past type errors in dev mode. Always check --strict or the equivalent.
After a successful build, verify the output directory matches expectations:
# For Next.js static export
OUTPUT_DIR="out"
# Verify critical files exist
REQUIRED_FILES=(
"index.html"
"404.html"
"_next/static"
"sitemap.xml"
"robots.txt"
"favicon.ico"
)
for file in "${REQUIRED_FILES[@]}"; do
if [ ! -e "$OUTPUT_DIR/$file" ]; then
echo "MISSING: $file"
fi
done
# Count HTML pages — compare against expected routes
ACTUAL_PAGES=$(find "$OUTPUT_DIR" -name "*.html" | wc -l)
echo "HTML pages found: $ACTUAL_PAGES"
Framework-specific output expectations:
| Framework | Output Dir | Key Artifacts |
|-----------|-----------|---------------|
| Next.js (static) | out/ | index.html, _next/static/, _next/data/ |
| Next.js (server) | .next/ | .next/server/, .next/static/ |
| Vite/React | dist/ | index.html, assets/ |
| Astro | dist/ | Per-route HTML, _astro/ |
| Cloudflare Pages | out/ or dist/ | _worker.js (if functions), _headers, _redirects |
Broken asset references are the most common post-build failure. An image path that works in dev (via dev server magic) but fails in production (because the path is wrong relative to the output).
# Extract all asset references from HTML files
grep -rhoP '(?:src|href)="([^"]*)"' "$OUTPUT_DIR"/*.html | \
sed 's/.*="\(.*\)"/\1/' | \
sort -u > /tmp/referenced-assets.txt
# Check each reference resolves
while read -r ref; do
# Skip external URLs, data URIs, and anchors
if [[ "$ref" =~ ^(http|https|data:|#|mailto:) ]]; then
continue
fi
# Resolve relative to output dir
target="$OUTPUT_DIR/$ref"
if [ ! -e "$target" ]; then
echo "BROKEN REF: $ref"
fi
done < /tmp/referenced-assets.txt
Common broken reference patterns:
/images/logo.png when the actual file is at /img/logo.pngmain.abc123.js) not matching because the hash changedSize regressions are silent killers. A single unguarded import can add 200KB.
# Capture current sizes
find "$OUTPUT_DIR" -type f -name "*.js" -exec du -k {} + | sort -rn > /tmp/js-sizes.txt
find "$OUTPUT_DIR" -type f -name "*.css" -exec du -k {} + | sort -rn > /tmp/css-sizes.txt
# Total JS payload
JS_TOTAL=$(find "$OUTPUT_DIR" -name "*.js" -exec cat {} + | wc -c)
echo "Total JS: $(echo "scale=1; $JS_TOTAL / 1024" | bc)KB"
# Compare against threshold
JS_LIMIT_KB=500
JS_ACTUAL_KB=$((JS_TOTAL / 1024))
if [ "$JS_ACTUAL_KB" -gt "$JS_LIMIT_KB" ]; then
echo "WARNING: JS bundle ($JS_ACTUAL_KB KB) exceeds limit ($JS_LIMIT_KB KB)"
fi
Size budget guidance:
| Asset Type | Warning Threshold | Hard Limit | Why | |-----------|-------------------|------------|-----| | Total JS | 300KB gzipped | 500KB gzipped | LCP impact on mobile | | Single chunk | 150KB gzipped | 250KB gzipped | Parse time | | Total CSS | 50KB gzipped | 100KB gzipped | Render blocking | | Individual image | 200KB | 500KB | Layout shift, bandwidth | | Total HTML per page | 100KB | 200KB | Time to first byte |
# Validate sitemap.xml is well-formed XML
xmllint --noout "$OUTPUT_DIR/sitemap.xml" 2>&1
# Extract URLs from sitemap and verify they correspond to real pages
grep -oP '<loc>\K[^<]+' "$OUTPUT_DIR/sitemap.xml" | while read -r url; do
# Convert URL to local path
path=$(echo "$url" | sed 's|https\?://[^/]*||')
local_path="$OUTPUT_DIR${path}index.html"
if [ ! -f "$local_path" ] && [ ! -f "$OUTPUT_DIR${path%.html}.html" ]; then
echo "SITEMAP ORPHAN: $url has no corresponding page"
fi
done
# Check robots.txt
if [ -f "$OUTPUT_DIR/robots.txt" ]; then
echo "robots.txt exists"
# Verify sitemap reference
grep -q "Sitemap:" "$OUTPUT_DIR/robots.txt" || echo "WARNING: robots.txt missing Sitemap directive"
else
echo "WARNING: No robots.txt found"
fi
For statically exported sites, every route must produce an HTML file:
# Extract all internal links from the built site
grep -rhoP 'href="(/[^"]*)"' "$OUTPUT_DIR" | \
sed 's/href="//;s/"//' | \
sort -u > /tmp/internal-links.txt
# Check each link resolves to a file
while read -r link; do
# Try both /path/index.html and /path.html
if [ ! -f "$OUTPUT_DIR${link}index.html" ] && \
[ ! -f "$OUTPUT_DIR${link}.html" ] && \
[ ! -f "$OUTPUT_DIR${link}" ]; then
echo "DEAD INTERNAL LINK: $link"
fi
done < /tmp/internal-links.txt
| Condition | Verdict | Reasoning | |-----------|---------|-----------| | Build exits non-zero | FAIL | Obvious. Nothing to verify. | | TypeScript errors in output | FAIL | Type errors are bugs. Period. | | Missing route HTML file | FAIL | 404 in production. | | Broken asset reference | FAIL | Visible breakage to users. | | Bundle size exceeds hard limit | FAIL | Performance degradation ships. | | Bundle size exceeds warning threshold | WARN | Investigate, but may be intentional. | | Deprecation warnings | WARN | Track, schedule cleanup. | | New unrecognized file type in output | WARN | Could be intentional, could be accidental. | | Sitemap URL with no page | WARN | SEO issue, not user-facing breakage. |
When comparing against a baseline:
{ "files": [...], "sizes": {...}, "routes": [...] }What it looks like: CI shows green checkmark because npm run build exited 0.
Why wrong: A zero exit code means the build tool finished. It does not mean the output is correct, complete, or deployable. Frameworks routinely exit 0 while emitting warnings about missing pages, oversized bundles, or unresolved imports that fall back to runtime errors.
Instead: Run explicit post-build verification. The build command is step 1. Verification is step 2. They are not the same step.
What it looks like: Developer opens the built site in a browser, clicks around for 30 seconds, says "looks good."
Why wrong: Humans cannot visually verify 50 routes, 200 asset references, and bundle sizes in 30 seconds. They check the homepage and maybe one other page.
Instead: Automated scripts that exhaustively check every route and asset. Human review is for visual fidelity, not structural integrity.
What it looks like: Build output shows 47 warnings. Developer says "those have always been there."
Why wrong: Warning count creep is how technical debt compounds. Each warning was once new. Nobody investigated it. Now there are 47 and nobody reads them. The 48th warning — the one that actually matters — is invisible.
Instead: Treat warning count as a ratchet. Today's count is the ceiling. New warnings must be resolved or explicitly acknowledged before merge. Track warning count in CI and fail if it increases.
What it looks like: "Our bundle limit is 500KB" applied uniformly to every page.
Why wrong: A landing page and an admin dashboard have different size budgets. The landing page needs to be fast for first-time visitors on mobile. The admin dashboard is used by logged-in users on desktop who will tolerate a larger initial load.
Instead: Per-route or per-entry-point size budgets. Use tools like bundlesize, size-limit, or next/bundle-analyzer to set granular limits.
Before declaring a build verified:
_redirects, _headers, _worker.js)This skill produces:
references/build-manifest-schema.md -- JSON schema for build baseline manifests used in comparisonsreferences/framework-output-maps.md -- Expected output structures for Next.js, Vite, Astro, and Cloudflare Pagestools
Building resilient distributed systems with circuit breakers, retries with full-jitter exponential backoff, retry budgets (per-request 3-attempt + per-client 10% ratio per Google SRE), deadline propagation, and the cascading-failure math (4 layers × 3 retries = 64x amplification). Grounded in Resilience4j, Microsoft Cloud Patterns, AWS Architecture Blog (Marc Brooker), and Google SRE Book.
testing
Designing HTTP cache headers that work correctly across browsers, CDNs, and shared proxies — `Cache-Control` directives per RFC 9111, `stale-while-revalidate` and `stale-if-error` per RFC 5861, the Vary header for varying responses, and surrogate keys for tag-based purging. Grounded in IETF RFCs and Cloudflare/Fastly docs.
development
Use when designing or fixing a Content Security Policy on a real site, choosing between nonce-based and hash-based CSP, adding strict-dynamic, debugging "Refused to execute inline script" errors, deploying CSP in report-only mode first, configuring report-to / report-uri, or auditing an existing policy for unsafe-inline / unsafe-eval / wildcards. Triggers: "CSP blocks legitimate inline script", strict-dynamic, nonce-{RANDOM}, sha256-{HASH}, object-src none, base-uri none, frame-ancestors, Trusted Types, X-Content-Security-Policy obsolete, report-only vs enforced. NOT for general HTTP security headers (HSTS, COOP/COEP), Trusted Types deep dive, CORS configuration, or building a WAF.
tools
Choosing and operating an HTTP API versioning strategy that doesn't break clients — Stripe's date-based pinned versions, the Deprecation/Sunset header pair (RFC 9745 + RFC 8594), URI vs header vs media-type approaches, and the version-transformer pattern. Grounded in Stripe's published architecture and IETF RFCs.