Skip to main content

QA SIGN-OFF: CHANGES REQUESTED

PR: churchwiseai-web #1724 feat/churchwise-site @ 7194c17 → main Reviewer: independent QA session (did not author the PR) · Date: 2026-09-25 Scope: Verification Ladder rung 3b (middleware / shared code touches every customer) + spec knowledge/acceptance/churchwise-compliance-kit.md

Verdict​

Not mergeable as-is. One finding is blocking (CI test job is red on a shared-contract test), one is important (a privacy-page claim the code contradicts on the real host). The calculator itself, the disclaimer gates, the "AI-quiet" copy and the shared-code isolation all held up under an independent click-through. Fix 1 and 2, re-push, and this becomes an APPROVED with the two post-merge conditions at the bottom.

Findings​

  1. BLOCKING · src/lib/pro-website-host-resolve.ts:30 — PLATFORM_HOST_SUFFIXES twin list was not updated with 'churchwise.ca'. src/lib/__tests__/pro-website-host-resolve.test.ts ("mirrors every platform host middleware.ts protects") fails locally and in CI (test job on the PR head: 3402 pass / 1 fail, run 36158947743). Effect on the real host: sitemap.ts, robots.ts and llms.txt call resolveProWebsiteHost() / proWebsiteDoc() BEFORE their host branches, so churchwise.ca would first do a needless premium_churches custom-domain lookup and only then fall through. The contract exists so a brand host can never be classified as a customer domain. Fix: add 'churchwise.ca' to the list at line 30 (same position as the middleware list), re-run the test. CLAUDE.md: CI must be green before merge.

  2. IMPORTANT · src/app/churchwise/privacy/page.tsx:49-52 vs src/components/PostHogProvider.tsx:86-100 — the privacy page says "ChurchWise's tools do not use tracking cookies. Aggregate visit counts do not rely on a per-visitor identifier." PostHogProvider excludes only Pro Website routes and Wise Dating Prep; on churchwise.ca it will posthog.init with autocapture: true and persistence: 'localStorage+cookie', which writes a per-visitor distinct_id cookie. The CookieConsent banner is <ApexOnly> (FIRST_PARTY_HOSTS = churchwiseai.com only), so a churchwise.ca visitor is never shown a consent choice and cannot decline. GoogleTags / MetaPixel / AttributionCapture have the same gap if their env vars are set. Could not observe empirically (PostHog skips automated browsers) — verified by code reading. Fix: add an isChurchWiseRoute() exclusion mirroring isWiseDatingPrepRoute() (host churchwise.ca / churchwise.localhost, path /churchwise or /churchwise/*) to PostHogProvider, GoogleTags, MetaPixel and AttributionCapture — this is the honest reading of spec items 11 and 13. Alternatively rewrite the privacy copy to disclose PostHog and ship a consent banner on that host; the exclusion is simpler and matches the brand.

  3. MINOR · src/app/churchwise/privacy/page.tsx:40 — customer-facing copy contains "(spec item 13)". Remove the internal reference.

  4. MINOR · src/app/churchwise/layout.tsx:45, src/app/churchwise/page.tsx:30, 55, 186-189, src/app/llms.txt/route.ts:226 — "checked monthly" / "checked every month" / "Re-checked monthly … every rule is checked again each month" state a process as existing. No re-check job exists in this PR (src/app/api/cron and vercel.json have no churchwise reference; positive control on the same grep hit daily-audit). The spec's Accuracy gate §5 lists the monthly job as required before go-live. Fix: either ship the source re-fetch cron (flags a founder action item on any content/HTTP change) or soften the copy to a commitment ("we re-check…") and log a FOUNDER_ACTIONS item with the first re-check date. Not blocking on its own, but the page should not claim more than the system does.

  5. MINOR · src/components/SupportWidgets.tsx:30 — the ChurchWiseAI "Open chat" bubble and cookie banner render on every /churchwise* page on Vercel previews (observed on all four pages). On churchwise.ca <ApexOnly> suppresses them, so this is a preview-only leak — but it is exactly the wisedatingprep case that was fixed here on 2026-08-06, and every preview-based verification of an "AI-quiet" brand currently shows an AI chat bubble. Fix: add pathname.startsWith('/churchwise') to the early return.

  6. MINOR · src/app/robots.ts:127, src/app/sitemap.ts:614, src/app/llms.txt/route.ts:493 — these use host.includes('churchwise.ca') while the middleware deliberately uses an exact match (/^(www\.)?churchwise\.ca(:\d+)?$/). Low risk today because resolveProWebsiteHost() runs first for customer domains, but a custom domain such as firstchurchwise.ca would receive ChurchWise's sitemap/robots/llms.txt the moment that lookup returns null. Use the same exact match for consistency with the PR's own reasoning.

What was verified and passed​

  • Shared code isolation. PLATFORM_HOST_SUFFIXES is only consumed by isPlatformHost() (suffix/equality match on the request host) in §00, §0-live and §0-cd; adding churchwise.ca can only change classification of a request whose host is churchwise.ca or *.churchwise.ca. §0d2 is entirely inside an exact-host guard. next.config.ts redirects are host-scoped (www.churchwise.ca; (www.)?churchwiseai.com + /churchwise*). bare-routes.ts startsWith('/churchwise') matches only src/app/churchwise/. brands/churchwise.css is scoped under [data-brand='churchwise']; churchwise.css has no unscoped :root/body/html rules (positive control ran).
  • Tests run by me: clergy-housing.test.ts + churchwise-host-mapping.test.ts 75/75 (includes sibling-host regression cases). Existing host/middleware suites (pro-website-domain-redirect, shared-api-prefixes, brand-static-asset-paths, brand-prefix-canonical-links, brand-from-host, wiseaiagency-www-canonical, robots-ai-crawlers, pro-website-host-resolve) 100/101 — the one failure is Finding 1. Typecheck node node_modules/typescript/bin/tsc --noEmit -p tsconfig.json exit 0.
  • Preview click-through (own Playwright script, 52 checks, 49 pass; the 3 "fails" are Finding 5 ×2 and a false negative from CSS text-transform on the eyebrow, confirmed against raw HTML):
    • /churchwise and /churchwise/clergy-housing-calculator 200; title "ChurchWise — …" (not the ChurchWiseAI template); no .cwa-chrome, brand-bare marker present; "Coming soon" ×3.
    • No dollar amount without the gates: nothing renders before the disclaimer checkbox; unticking it removes the amount; fractional (6.5) and zero months → no amount; two-clergy spouse → refer panel, inputs hidden, no $ figure anywhere in the result region; Québec → federal amount shown with an "Also worth knowing" refer panel; US reasonable-pay has no default and unanswered / "Not sure" / "No" all show no amount and hide the inputs; retired → refer.
    • Values: Canada 60k/12mo/12k → CA$12,000.00; 50k/20k → CA$16,666.67 (line 2 binding); US 24k/24k/24k/30k → $24,000.00; US paid 30k/used 20k → exclusion $20,000.00, excess $4,000.00.
    • Labels match the rules: Canada breakdown shows Line 1 … Line 10 with the T1223 wording; US shows designated / paid / allowance received / used / FRV / exclusion / excess (Form 1040 line 1h). "Rules this tool follows" 21 rows, each with Checked 2026-09-25; "Special situations" 6 rows, each with a source link; FAQ contains one entry per refer rule.
    • "AI" wording: on the visible text the only whole-word hits are the FAQ item ("Is this made with AI?" / "including AI") and "ChurchWiseAI Ltd"; none in meta tags; JSON-LD (Organization, WebApplication, HowTo, FAQPage) clean apart from the same two.
    • 375px: no horizontal overflow on home or calculator. JS off: direct-answer paragraph, h1, rules with dates and the disclaimer all server-rendered; no amount server-rendered.
    • Privacy claim "not sent anywhere": typed unique values into the calculator and watched every request — none carried them; the component is client-only with no fetch.
    • Existing pages unchanged on the preview: / and /pricing 200 with CWA chrome, no page errors; /s/grace-community (demo church) 200 with Pro Website chrome; /funeralwiseai, /vetwiseai, /wiseaiagency 200 with their own chrome; /sitemap.xml, /robots.txt, /llms.txt on the preview host contain no churchwise.ca output.
  • Evidence: screenshots home.png, calc-canada.png, calc-refer.png, calc-us-unanswered.png, calc-us.png, calc-375.png, pricing.png, s-slug.png in the QA session scratchpad (…/scratchpad/qa1724-out/); scripts qa1724.mjs, qa1724b.mjs, qa1724c.mjs alongside.

Observations (not defects)​

  • On the preview host the header/footer links (/clergy-housing-calculator, /privacy, /terms) 404 and Next's prefetch logs a console 404 — expected, the preview has no host rewrite; §0d2 rewrites them on churchwise.ca. This is why the real-host click-through after merge is not optional.
  • Ordering: from the moment this merges, churchwiseai.com/churchwise/* 301s to churchwise.ca. If the domain is not yet attached to the Vercel project at that moment, that redirect lands on a Vercel 404. Attach churchwise.ca + www.churchwise.ca BEFORE merging (the PR already lists this; keep the order).

Conditions for the eventual APPROVED​

  1. Findings 1 and 2 fixed and pushed; test job green.
  2. After merge and domain attach: click-through on https://churchwise.ca (rewrite, nav links, www→apex, churchwiseai.com/churchwise → churchwise.ca, sitemap/robots/llms.txt host output) and the paying-customer smoke from ai-company-os/company/PAYING-CUSTOMER-SURFACES.md within 15 minutes — middleware changed.

QA SIGN-OFF: APPROVED

Re-review · 2026-09-25 (second pass) · PR #1724 feat/churchwise-site @ 03908574b → main Reviewer: independent QA session (did not author the PR or the fixes) · Supersedes the CHANGES REQUESTED verdict on 7194c17 above.

Verdict​

Approved for merge, on the two post-merge conditions at the bottom (both already in the PR checklist). All six earlier findings are resolved or founder-overridden, the shared-code changes do not alter any other host's behaviour, and CI is green on this head.

Findings from the first pass, re-checked​

#StatusEvidence
1 Twin PLATFORM_HOST_SUFFIXESFixedsrc/lib/pro-website-host-resolve.ts:41 now lists churchwise.ca; pro-website-host-resolve.test.ts passes locally; CI test job success on 03908574b (run 36166736826, previously 1 fail).
2 Privacy copy vs analyticsFounder override, verifiedFounder chose "do what we do for all our sites": the ChurchWise privacy/terms pages are deleted (/churchwise/privacy, /churchwise/terms → 404 on the preview; on churchwise.ca next.config.ts:757-768 301s /privacy and /terms to churchwiseai.com's pages, host-guarded with (www\.)?churchwise\.ca, which Next.js anchors as ^…$ — prepare-destination.js:101). Footer links are absolute https://churchwiseai.com/privacy / /terms (observed in the DOM). The CookieConsent banner shows on both ChurchWise pages ('consent-only' mode) and the ChatWidget bubble does not (observed). .cw-calc-panel carries ph-no-capture (observed in the DOM on the calculator page). The remaining company-level mismatch (churchwiseai.com/privacy says "We do not use advertising, tracking, or analytics cookies" while PostHog sets ph_…_posthog) is pre-existing, out of this PR's scope, and logged: FOUNDER_ACTIONS.md:4416 "P1 — Privacy policy says 'no analytics cookies' but analytics run (added 2026-09-25)".
3 "(spec item 13)" in customer copyMootThe page that contained it was deleted. grep "spec item" over src/app/churchwise → only code comments.
4 "checked monthly" claimsFixedLayout description, home description, hero lede, the "3. Dated and re-checked" card and the llms.txt summary now say each rule shows the date it was last checked and that re-checks happen before tax season / when the law changes — true today. grep -i "monthly|every month" over src/app/churchwise + the CHURCHWISE llms doc → 0 hits (positive control on the same grep: changeFrequency: 'monthly' in sitemap.ts).
5 Chat bubble on /churchwise*FixedsupportWidgetsModeFor() returns consent-only for the ChurchWise host or path; observed on the preview: banner present, zero button.fixed bubbles on /churchwise and /churchwise/clergy-housing-calculator.
6 includes('churchwise.ca')FixedOne shared isChurchWiseHost() (src/lib/churchwise/host.ts) used by middleware.ts, robots.ts, sitemap.ts, llms.txt/route.ts; host.test.ts covers firstchurchwise.ca → false.

Shared-code impact re-verified (every customer)​

  • SupportWidgets / supportWidgetsModeFor (replaces <ApexOnly>): evaluated the pure function directly against teambeckett.ca, www.teambeckett.ca, grace-community.john316.church, beckwithhillscrc.org, funeralwiseai.com, sermonwise.ai, firstchurchwise.ca → none (no banner, no bubble — same as before); churchwiseai.com /, /pricing, /churchwiseai-something → full (same as before); churchwiseai.com/churchwise and churchwise.ca/* → consent-only. /founder* and /wisedatingprep* still none. SSR still renders nothing (mode defaults to none until the effect runs), so hydration behaviour is unchanged. ChatWidget's own isFirstPartyHost guard is untouched, so the bubble stays double-gated on customer hosts. support-widgets-mode.test.ts 9/9.
  • Observed on the preview (churchwiseai-ewaifce1s): / and /pricing → banner + "Open chat" bubble (unchanged); /s/grace-community → no banner, one "Open chat" bubble — identical to production churchwiseai.com/s/grace-community on main (that bubble is the Pro Website's own church chatbot, not the CWA widget); /funeralwiseai → its own "Open FuneralWiseAI Assistant" bubble, no banner (unchanged); /wisedatingprep → nothing (unchanged).
  • PostHog: no code change beyond a comment; churchwise.ca now runs analytics like every other ChurchWiseAI surface, per founder. GoogleTags / MetaPixel unchanged.
  • Middleware: §0d2 body is byte-identical apart from isChurchWiseHost(hostname) replacing the inline regex (same pattern). PLATFORM_HOST_SUFFIXES consumers unchanged.
  • Sitemap/robots/llms.txt on the preview host: no churchwise.ca output (unchanged for every non-ChurchWise host). ChurchWise sitemap no longer lists /privacy//terms.
  • next.config.ts: the two new redirects are host-guarded to churchwise.ca only; the (www\.)? group is safe here because the destination is a different domain (no loop).

Tests and checks run by me at 03908574b​

  • node --env-file=.env.local --import tsx --test over: clergy-housing.test.ts, churchwise/__tests__/host.test.ts, churchwise-host-mapping.test.ts, support-widgets-mode.test.ts, pro-website-host-resolve.test.ts, pro-website-domain-redirect.test.ts, shared-api-prefixes.test.ts, brand-static-asset-paths.test.ts, brand-prefix-canonical-links.contract.test.ts, brand-from-host.test.ts, wiseaiagency-www-canonical.test.ts, robots-ai-crawlers.test.ts, bare-routes.test.ts → 198/198.
  • Typecheck node node_modules/typescript/bin/tsc --noEmit -p tsconfig.json → exit 0.
  • CI: Tests success, Critical Path Gate, Critical Path Protection, Knowledge Sync Gate all success on this head. The four churchwise suites are now in .github/workflows/test.yml:211-214.
  • Full 52-check Playwright regression from the first pass re-run on the new preview → 51/52; the single "fail" is the same JS-off false negative (the eyebrow's CSS text-transform changes innerText; raw HTML contains "Rules this tool follows" once). Every calculator gate, value, label, "AI"-word, 375px and sibling-page check passed exactly as before. Home footer hrefs now: https://churchwiseai.com/privacy, https://churchwiseai.com/terms; the banner's link is the relative /privacy (redirected on churchwise.ca by next.config.ts).

Honest limitation​

I could not observe PostHog event traffic from an automated browser: posthog-js never emitted a POST in headless Chromium even with navigator.webdriver spoofed, consent pre-accepted and the site's cwa-analytics-verify=capture override set (only the asset/config GETs fired). So the ph-no-capture exclusion of typed and calculated values is code-verified, not observed: the class is present on the panel, and PostHog documents that autocapture ignores and session recording blocks ph-no-capture subtrees. Note posthog-recorder.js IS loaded on this project, so session recording is on and the class is load-bearing. A real-browser spot check after launch (open a replay of a churchwise.ca calculator session in PostHog and confirm the panel is blocked) would close this; it is a 2-minute founder/agent task, not a merge blocker.

Conditions (already in the PR checklist)​

  1. Attach churchwise.ca + www.churchwise.ca to the Vercel project BEFORE merging — from merge time churchwiseai.com/churchwise/* 301s to churchwise.ca.
  2. After merge: click-through on the real host (rewrite, nav links, www→apex, /privacy and /terms 301s to churchwiseai.com, banner shown and no bubble, sitemap/robots/llms.txt host output) plus the paying-customer smoke from ai-company-os/company/PAYING-CUSTOMER-SURFACES.md within 15 minutes — middleware changed.