Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"client:proof-review": "node scripts/review-client-proof.mjs",
"retention:automation-check": "node scripts/check-retention-automation.mjs",
"site:prepare": "node scripts/prepare-static-site-bundle.mjs",
"site:check-live-heading-hierarchy": "node scripts/test-public-live-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
1 change: 1 addition & 0 deletions scripts/test-public-heading-hierarchy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,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",
Expand Down
140 changes: 140 additions & 0 deletions scripts/test-public-live-heading-hierarchy.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Guard the LIVE public site against skipped heading levels on the pages
// that carry the repaired outline in source.
//
// It runs as `npm run site:check-live-heading-hierarchy`, from the deploy
// lane's post-deploy verification, and on demand. It is deliberately NOT
Comment on lines +4 to +5

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 Invoke the new guard from post-deploy verification

The inspected .github/workflows/deploy-public-site.yml runs publish-public-site.mjs --deploy, but verifyLive in that publisher invokes only check-public-live-deploy.mjs; a repo-wide search finds no deployment-path invocation of this new script. Consequently the added Drishti, Privacy Choices, and stylesheet checks never run after a deployment despite this stated contract, so the post-deploy verifier needs to call the guard explicitly.

Useful? React with 👍 / 👎.

// 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.
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 pages 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 pages 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. /promptly/support/ is the page named by the
// 2026-08-08 scout item (repaired in source by PR #20); it must not regress
// on the live site either.
const LIVE_PAGES = [
{
name: "Promptly support",
url: "https://tinystudio.in/promptly/support/",
source: "public/promptly/support/index.html"
},
{
name: "Drishti support",
url: "https://tinystudio.in/drishti/support/",
source: "public/drishti/support/index.html"
},
{
name: "Privacy Choices",
url: "https://tinystudio.in/privacy-choices/",
source: "public/privacy-choices/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 Keep card-heading matches within each article

When an .info-card lacks an H2 but any later section contains one, [\s\S]*? crosses the closing </article> and counts that later heading as the card title. For example, if the final card regresses to H3 and an additional section H2 appears before the footer, this count remains three, the total H2 count remains at least four, and the no-jump check can also pass, so the live guard misses the card-heading regression it claims to detect. Restrict each match to the corresponding article contents or parse the HTML structure.

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 +69 to +71

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 Treat non-2xx live responses as failures

If a monitored page is deleted or misrouted and returns 404, this branch returns null, so the caller runs no assertions and the guard can exit successfully. That hides a production regression affecting the exact support pages this live check is intended to protect; HTTP error responses should increment failures rather than be treated like transport-level unreachability.

Useful? React with 👍 / 👎.

}
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-heading-hierarchy: the deployed tinystudio.in pages keep the repaired heading outline (no skipped levels)")

console.log("A. live pages carry 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("C. npm test/ci wiring")
const pkg = JSON.parse(read("package.json"))
ok(
pkg.scripts.test.includes("test-public-live-heading-hierarchy.mjs"),
"npm test runs the public live heading-hierarchy guard"
Comment on lines +130 to +132

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 Remove the assertion that makes the standalone guard fail

Once package.json is made parseable, this condition is always false because the unchanged scripts.test command does not contain test-public-live-heading-hierarchy.mjs. The dedicated npm run site:check-live-heading-hierarchy command therefore exits with a failure even when every live assertion passes; either remove these wiring assertions, as the file header says the guard is deliberately standalone, or actually align the intended wiring.

Useful? React with 👍 / 👎.

)
ok(
pkg.scripts.ci.includes("test-public-live-heading-hierarchy.mjs"),
"npm run ci runs the public live heading-hierarchy guard"
)

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