Skip to content
This repository was archived by the owner on Oct 12, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/live-site-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,13 @@ jobs:
cache: npm
- run: npm ci --ignore-scripts
- run: node scripts/check-public-live-soft-404.mjs
live-contact-heading-hierarchy:
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-contact-heading-hierarchy.mjs
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,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-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",
"send:configure": "node scripts/configure-sender-setup.mjs",
Expand Down
9 changes: 9 additions & 0 deletions scripts/check-public-live-deploy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
// 3. unknown URLs get a real 404, not the homepage (PR #34)
// 4. homepage stays portfolio-only: brand-disambiguation copy (#29) live,
// and no managed-service buyer-path content (#10/#11, snoozed).
// Proof 2b covers the 2026-08-08 dogfood finding page:
// 2b. /contact/ renders H2 after H1 (the heading-hierarchy repair, PR #18).
import { join } from "node:path"
import { fileURLToPath } from "node:url"
import { dirname } from "node:path"
Expand Down Expand Up @@ -71,6 +73,13 @@ try {
ok(body.includes("application/ld+json"), "page contains application/ld+json")
}

console.log("B2. /contact/ renders H2 after H1 (heading-hierarchy repair, PR #18)")
{
const { status, body } = await get("/contact/")
ok(status === 200, `/contact/ returns 200 (got ${status})`)
ok(h2AfterFirstH1(body), "H2 follows the first H1 before any H3")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify every contact card heading after deployment

In the deploy-public-site.yml publish path, this is the new post-upload acceptance assertion, but h2AfterFirstH1 only proves that some H2 precedes the first H3. A partially stale /contact/—for example, one with an earlier managed-service H2 or only the first card promoted while later card titles remain H3—passes this check even though the heading-hierarchy repair is not deployed, causing the release lane to report the publish as verified. Apply the three-card and no-jump assertions used by the nightly guard here as well.

Useful? React with 👍 / 👎.

}

console.log("C. unknown URLs return a real 404 (PR #34)")
{
const probe = `/definitely-missing-live-deploy-${Date.now()}`
Expand Down
139 changes: 139 additions & 0 deletions scripts/test-public-live-contact-heading-hierarchy.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Guard the LIVE tinystudio.in /contact/ page against the heading-hierarchy
// finding that was repaired in source but still ships on the deployed site.
//
// The 2026-08-08 dogfood finding "Heading hierarchy needs cleanup on /contact"
// (finding 52753880dfc7) was repaired in source by PR #18 (card headings
// promoted from H3 to H2, outline H1 -> H2s -> H3s with no jumps), and
// scripts/test-public-heading-hierarchy.mjs guards the worktree HTML. This
// check 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 as `npm run site:check-live-contact-heading-hierarchy`, from the
// nightly live-site-check workflow (the loud staleness alarm while the site
// is stale) and on demand. It is deliberately NOT part of `npm run 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: a page or stylesheet that is unreachable is skipped
// (ok, no assertion); a page that is reachable but stale fails loudly.
//
// Escape hatch for machines without network access:
// SKIP_LIVE_CHECKS=1 npm run site:check-live-contact-heading-hierarchy
//
// Only public tinystudio.in URLs and the local source files are referenced
// here; there is no per-environment configuration.
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")

if (process.env.SKIP_LIVE_CHECKS === "1") {
console.log("test-public-live-contact-heading-hierarchy: SKIP_LIVE_CHECKS=1, skipping live site checks")
process.exit(0)
}

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 /contact/ 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 what
// the live site serves.
const LIVE_PAGES = [
{
name: "Contact",
url: "https://tinystudio.in/contact/",
source: "public/contact/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(/<h([1-6])\b[^>]*>/gi)].map((m) => Number(m[1]))

// Count <h2> used as .info-card card titles (inside <article class="info-card">).
const infoCardTitleCount = (html) =>
(html.match(/<article class="info-card[^"]*"[^>]*>[\s\S]*?<h2\b/gi) || []).length

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound each card-heading match to its article

In the nightly live-contact-heading-hierarchy job, this regex can cross the closing </article> while searching for an H2. If the third contact card regresses to H3, its match consumes the footer H2 instead, so infoCardTitleCount still returns 3; the current page also retains enough other H2s, and the H2→H3→H2 sequence creates no upward level jump, allowing every outline assertion to pass. Restrict each match to the contents of its individual .info-card article so a partially regressed deployment fails the guard.

Useful? React with 👍 / 👎.


const fetchLive = async (url) => {
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
Comment on lines +77 to +80

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail closed when the monitored route is not its own 200 response

In the nightly live-contact-heading-hierarchy job, a reachable /contact/ that returns 404/500 is logged as an “ok skipped” result, while the default redirect-following behavior also accepts another page’s response. For example, a redirect to /promptly/ would pass because that page has the same one-H1, three-card-H2 outline, so a broken contact route can leave this job green. Require a 200 response from the expected final URL; reserve skipping only for the intended network-unavailable case.

Useful? React with 👍 / 👎.

}
Comment on lines +77 to +81

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fail when the live endpoint returns an HTTP error.

An HTTP response proves that the endpoint is reachable. Lines 78-80 convert a 404 or 500 response into a successful skip. The guard then exits successfully without validating a missing or failed deployment. Reserve skips for fetch errors and timeouts. Record a failed assertion for non-2xx responses.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-public-live-contact-heading-hierarchy.mjs` around lines 77 - 81,
Update the response handling around the fetch in the live endpoint test so
non-2xx responses record a failed assertion instead of logging a skip and
returning null. Keep skips limited to fetch errors and timeouts, while
preserving normal validation for successful responses.

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`)
Comment on lines +95 to +96

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope the footer H2 check to the footer

When the unfiltered contact page is deployed—or after the snoozed sections return—this count no longer verifies the claimed footer H2: the current source contains two additional non-card H2s, so changing the footer heading from H2 to H3 still leaves at least four H2s, while the no-jump check also passes. Inspect the footer heading directly so the nightly guard actually enforces the documented H2 footer -> H3 columns outline.

Useful? React with 👍 / 👎.

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)`)
Comment on lines +95 to +104

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Verify that no H2 follows the first H3.

The current checks accept H1 -> H2 -> H3 -> H2. That sequence has no upward level jump and still meets the H2 count. It violates the required flat H2 band before the footer H3 headings. After the first H3, fail if a later H2 exists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-public-live-contact-heading-hierarchy.mjs` around lines 95 -
104, Update the heading validation loop in
scripts/test-public-live-contact-heading-hierarchy.mjs to track whether the
first H3 has been seen and count any subsequent H2 as a failure. Add an
assertion that no H2 appears after the first H3, while preserving the existing
jump and H2-count checks.

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-contact-heading-hierarchy: the deployed tinystudio.in /contact/ page keeps the repaired heading outline (no skipped levels)")

console.log("A. live /contact/ carries the repaired heading hierarchy")
for (const page of LIVE_PAGES) {
const html = await fetchLive(page.url)
if (html !== null) assertRepairedOutline(page.name, 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("\nLive /contact/ heading-hierarchy guard result: the finding stays open against tinystudio.in until a refresh of the live deployment lands on origin/main.")
Comment on lines +132 to +137

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Report the result for the correct target.

The stylesheet message reports stale CSS when an earlier page assertion incremented global failures. Line 137 also reports that the finding remains open when all checks pass. Track failures per target, and select the final message from the final result.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-public-live-contact-heading-hierarchy.mjs` around lines 132 -
137, Update the heading-hierarchy guard in the test script to track failures
separately for each target, so stale-stylesheet messaging only reflects the
stylesheet check itself rather than earlier page assertions. Use the final
target result to choose the concluding message, reporting the finding as open
only when the relevant live deployment check fails and reporting success when
all checks pass.

console.log(`\n${checks} checks, ${failures} failures`)
process.exit(failures === 0 ? 0 : 1)
Loading