diff --git a/docs/operations/decisions/DR-003-website-stack-fingerprinting.md b/docs/operations/decisions/DR-003-website-stack-fingerprinting.md new file mode 100644 index 0000000..9dd3cf7 --- /dev/null +++ b/docs/operations/decisions/DR-003-website-stack-fingerprinting.md @@ -0,0 +1,43 @@ +# DR-003: Website Build-Stack Fingerprinting + SSR Signal + +**Date:** 2026-08-17 +**Status:** Accepted +**Supersedes:** nothing (extends v1.1 capture contract) + +## Context + +The Phoenix Salon + Spa audit (2026-08-17) shipped a finding — "no street +address on /contact" — that was false: the address exists but is rendered +client-side, so the pipeline's plain-HTTP capture did not see it and downstream +analysis could not distinguish "content missing" from "content present but +JS-rendered." The site was built on Lovable (Vite SPA behind Cloudflare); the +capture did not record that, so the fix path ("one-time build fix" vs "edit +content") had to be discovered by hand. + +## Decision + +1. `multi_scraper.py` records, at website capture time: + - `website.stack.detected` / `website.stack.primary` — build-stack + fingerprint (Lovable, v0, Next.js, Nuxt, Gatsby, Vite, React/Angular/Vue/ + Svelte, WordPress, Squarespace, Wix, Webflow, Shopify, hosts). + - `website.stack.html_word_count` / `website.stack.ssr_signal` — word count + a non-JavaScript crawler reads from the raw HTML (`weak` < 50 words). +2. SOP v1.2: the report states the build stack when known and flags weak SSR; + capture-based website findings must be qualified by SSR signal. +3. `multi_scraper.py --selftest` covers fingerprinting (Lovable fixture, weak + vs present SSR) and runs in CI-free environments without network. + +## Rationale + +Knowing how a site is built determines (a) what the capture can and cannot +see, and (b) the cheapest correct fix. Regex fingerprinting on raw HTML is +deliberate: known-site audits need a label, not a scanner — false negatives +land in `unknown`, never a confident wrong answer. + +## Verification + +- `--selftest`: PASS (Lovable fixture → primary=lovable, ssr=weak; 80-word + page → ssr=present, primary=unknown). +- Live re-capture of phoenixcameronpark.com: `detected=['lovable']`, + `html_word_count=13`, `ssr_signal=weak` — matches the manual Playwright + audit of the same day.