diff --git a/.github/workflows/live-site-check.yml b/.github/workflows/live-site-check.yml index 40c5d581..ed8704bc 100644 --- a/.github/workflows/live-site-check.yml +++ b/.github/workflows/live-site-check.yml @@ -36,6 +36,17 @@ jobs: cache: npm - run: npm ci --ignore-scripts - run: node scripts/check-public-live-soft-404.mjs + live-promptly-support-heading: + runs-on: [self-hosted, linux, x64, vps-verify] + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 20 + cache: npm + - run: npm ci --ignore-scripts + - run: node scripts/test-public-live-promptly-support-heading.mjs + live-contact-heading-hierarchy: runs-on: [self-hosted, linux, x64, vps-verify] steps: diff --git a/package.json b/package.json index b64df301..4f37df78 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "retention:automation-check": "node scripts/check-retention-automation.mjs", "site:prepare": "node scripts/prepare-static-site-bundle.mjs", "site:check-live": "node scripts/check-public-live-soft-404.mjs", + "site:check-live-promptly-support-heading": "node scripts/test-public-live-promptly-support-heading.mjs", "site:check-live-contact-heading-hierarchy": "node scripts/test-public-live-contact-heading-hierarchy.mjs", "claims:check": "node scripts/check-outbound-claim-safety.mjs", "config:check": "node scripts/check-agency-defaults.mjs", diff --git a/scripts/test-public-heading-hierarchy.mjs b/scripts/test-public-heading-hierarchy.mjs index 58fe70e0..5ede2b2c 100644 --- a/scripts/test-public-heading-hierarchy.mjs +++ b/scripts/test-public-heading-hierarchy.mjs @@ -25,6 +25,7 @@ const AFFECTED_PAGES = [ "public/contact/index.html", "public/promptly/index.html", "public/promptly/privacy/index.html", + "public/promptly/support/index.html", "public/drishti/index.html", "public/drishti/support/index.html", "public/privacy-choices/index.html", diff --git a/scripts/test-public-live-promptly-support-heading.mjs b/scripts/test-public-live-promptly-support-heading.mjs new file mode 100644 index 00000000..294e27ca --- /dev/null +++ b/scripts/test-public-live-promptly-support-heading.mjs @@ -0,0 +1,137 @@ +// Guard the LIVE Promptly support page against skipped heading levels. +// +// The 2026-08-08 scout item named /promptly/support/: it was repaired in +// source by PR #20 (card headings promoted from H3 to H2, outline +// H1 -> H2x3 -> footer H2 -> H3s with no jumps) and origin/main carries the +// fixed outline. This guard re-asserts the repaired outline against the +// page the live site actually serves, so a stale deployment (like the +// June-20 bundle still serving H3 cards) fails loudly instead of silently +// re-opening the finding. +// +// It runs standalone as `npm run site:check-live-promptly-support-heading` +// and from the nightly live-site-check.yml staleness net. It is +// deliberately NOT part of `npm test` / `npm run ci`: those blocking +// chains must stay green on repo state alone, while the live site is +// deployed by an external mechanism (Cloudflare Pages). Blocking CI on the +// live site would keep every pull request red whenever the deployment is +// stale, and would deadlock the deploy lane's pre-deploy `npm run check` +// gate. Network-tolerant: skips when the site is unreachable, fails loudly +// while it serves the stale bundle. +import { readFileSync } from "node:fs" +import { fileURLToPath } from "node:url" +import { dirname, join } from "node:path" + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..") +const read = (p) => readFileSync(join(ROOT, p), "utf8") + +let failures = 0 +let checks = 0 +const ok = (cond, msg) => { + checks++ + if (cond) console.log(` ok ${msg}`) + else { + failures++ + console.error(` FAIL ${msg}`) + } +} + +// The deployed page must keep the repaired heading outline (H1 -> H2 cards +// -> H2 footer -> H3 footer columns, no skipped levels). The local suite +// (test-public-heading-hierarchy.mjs) asserts the same outline against the +// worktree HTML; this guard re-asserts it against the page the live site +// actually serves, so a stale deployment (like the June-20 bundle still +// serving H3 cards) fails loudly instead of silently re-opening the +// skipped-heading-level finding from the 2026-08-08 scout item. +const LIVE_PAGE = { + name: "Promptly support", + url: "https://tinystudio.in/promptly/support/", + source: "public/promptly/support/index.html" +} +const LIVE_CSS_URL = "https://tinystudio.in/styles.css" +const FETCH_TIMEOUT_MS = 10_000 + +// Heading levels in document order, e.g. [1, 2, 2, 2, 2, 3, 3, 3]. +const headingLevelsOf = (html) => + [...html.matchAll(/]*>/gi)].map((m) => Number(m[1])) + +// Count

used as .info-card card titles (inside
). +const infoCardTitleCount = (html) => + (html.match(/
]*>[\s\S]*? { + try { + const res = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }) + if (!res.ok) { + console.log(` ok skipped: ${url} answered ${res.status}, deployment not reachable - no assertions run for it`) + return null + } + return await res.text() + } catch (err) { + console.log(` ok skipped: ${url} unreachable (${err?.cause?.code ?? err?.name ?? "network error"}) - no assertions run for it`) + return null + } +} + +const assertRepairedOutline = (name, source, html) => { + const levels = headingLevelsOf(html) + ok(levels.length > 0, `live ${name} page contains at least one heading`) + ok(levels.filter((l) => l === 1).length === 1, `live ${name} page has exactly one H1`) + ok(levels[0] === 1, `live ${name} page has the H1 as the first heading in the outline`) + ok(infoCardTitleCount(html) === 3, `live ${name} page has the three card headings as H2s inside .info-card articles`) + const cardH2s = levels.filter((l) => l === 2).length + ok(cardH2s >= 4, `live ${name} page keeps the flat H2 band (card H2s plus the footer H2) before the footer H3s`) + let jumps = 0 + for (let i = 1; i < levels.length; i++) { + if (levels[i] - levels[i - 1] > 1) { + jumps++ + console.error(` bad transition H${levels[i - 1]} -> H${levels[i]} on ${name}`) + } + } + ok(jumps === 0, `live ${name} page has no heading-level jump greater than one (no H1 -> H3 skip)`) + if (failures > 0) { + console.error(` the deployed ${source} is stale: it misses the heading-hierarchy repair that the worktree copy of ${source} already has. Refresh the live deployment from origin/main.`) + } +} + +console.log("test-public-live-promptly-support-heading: the deployed tinystudio.in Promptly support page keeps the repaired heading outline (no skipped levels)") + +console.log("A. live page carries the repaired heading hierarchy") +const html = await fetchLive(LIVE_PAGE.url) +if (html !== null) assertRepairedOutline(LIVE_PAGE.name, LIVE_PAGE.source, html) + +console.log("B. live stylesheet keeps the shared card-heading rule at the former card scale") +const css = await fetchLive(LIVE_CSS_URL) +if (css !== null) { + const ruleStart = css.indexOf(".info-card :is(h2, h3) {") + ok(ruleStart !== -1, "live styles.css defines .info-card :is(h2, h3) {") + const ruleEnd = ruleStart === -1 ? -1 : css.indexOf("}", ruleStart) + const ruleBody = ruleStart === -1 ? "" : css.slice(ruleStart, ruleEnd) + for (const decl of ["margin-top: 12px", "font-size: clamp(1.65rem, 2vw, 2.35rem)", "max-width: none"]) { + ok(ruleBody.includes(decl), `live card rule keeps ${decl}`) + } + ok( + !/\.info-card\s+h3\s*{/.test(css), + "live styles.css replaced the old .info-card h3-only rule with the shared :is(h2, h3) rule" + ) + if (failures > 0) { + console.error(" the deployed stylesheet is stale: it misses the card-heading pairing that public/styles.css already has. Refresh the live deployment from origin/main.") + } +} + +console.log("C. standalone script wiring (out of the blocking ci/test chain)") +const pkg = JSON.parse(read("package.json")) +ok( + pkg.scripts["site:check-live-promptly-support-heading"] === "node scripts/test-public-live-promptly-support-heading.mjs", + "npm run site:check-live-promptly-support-heading runs this guard on demand" +) +ok( + !pkg.scripts.ci.includes("test-public-live-promptly-support-heading.mjs"), + "npm run ci does not block on the live-site guard (blocking chain stays green on repo state alone)" +) +ok( + !pkg.scripts.test.includes("test-public-live-promptly-support-heading.mjs"), + "npm test does not block on the live-site guard" +) + +console.log(`\n${checks} checks, ${failures} failures`) +process.exit(failures === 0 ? 0 : 1)