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 Pagesdata-ai
license: Apache-2.0 NOT for unrelated tasks outside this domain.
development
Use when designing caching strategies (cache-aside, write-through, write-behind), implementing distributed locks, building rate limiters, leaderboards, real-time streams (XADD/consumer groups), pub/sub, or tuning eviction policies. Triggers: thundering-herd on cache miss, dogpile on key expiry, Redlock vs SET-NX-PX choice, sliding-window rate limiter, hot-key on a single cluster slot, big-key blowup, MULTI/EXEC across slots, KEYS in production. NOT for Redis Cluster operations/admin (different domain), embedded KV (SQLite, leveldb), in-process LRU caches, or Memcached.
tools
Drawing the `'use client'` boundary correctly in React Server Components apps (Next.js App Router, RSC frameworks) — leaf-pushing, slot composition, serialization rules, and environment poisoning prevention. Grounded in react.dev and Next.js 16 docs.
development
Use when designing rate limiting for an API, choosing between token bucket / sliding window / leaky bucket / fixed window, implementing it in Redis, deciding edge (Cloudflare/Upstash) vs origin enforcement, sizing per-user vs per-IP vs per-endpoint quotas, returning the right 429 response with Retry-After, or fixing the boundary-burst bug in fixed-window limiters. Triggers: 429 too many requests, INCR + EXPIRE, ZADD + ZREMRANGEBYSCORE + ZCARD, X-RateLimit-Remaining header, Cloudflare WAF rate limiting rules, Upstash @upstash/ratelimit, leaky bucket shaping vs policing, distributed rate limiter consistency. NOT for DDoS mitigation specifically (different scale), CAPTCHA / bot management, full WAF design, or per-user quota billing.