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",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Invoke the guard from post-deploy verification

This only exposes a manual npm command; the inspected publish path in .github/workflows/deploy-public-site.yml invokes publish-public-site.mjs, whose verifyLive() runs only check-public-live-deploy.mjs. A repo-wide reference search found no deployment invocation of this new command, so deployments with stale headings on these two pages can still complete without running the guard this change is intended to add.

Useful? React with 👍 / 👎.

"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
133 changes: 133 additions & 0 deletions scripts/test-public-live-heading-hierarchy.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// 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
// 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.
const LIVE_PAGES = [
{
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

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 +62 to +64

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 when a live endpoint returns an HTTP error

When either guarded page or styles.css responds with a 404 or 5xx, the server is reachable but this branch treats the response as an allowed skip and performs no assertions. Once the contradictory wiring assertions are corrected, a deployment that removes all three resources can therefore pass this live guard; only genuine transport failures should receive the intended network-tolerant handling, while non-2xx responses should count as failures.

Useful? React with 👍 / 👎.

}
Comment on lines +62 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Fail reachable HTTP error responses.

fetch() completed when res.ok is false. The deployment is reachable in this case. Lines 52-55 skip 404 and 5xx responses without incrementing failures, so the guard can exit successfully while a required public page or stylesheet is unavailable. Only transport and timeout errors should skip.

Proposed fix
     if (!res.ok) {
-      console.log(`  ok skipped: ${url} answered ${res.status}, deployment not reachable - no assertions run for it`)
+      ok(false, `live endpoint ${url} answered ${res.status}`)
       return null
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!res.ok) {
console.log(` ok skipped: ${url} answered ${res.status}, deployment not reachable - no assertions run for it`)
return null
}
if (!res.ok) {
ok(false, `live endpoint ${url} answered ${res.status}`)
return null
}
🤖 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-heading-hierarchy.mjs` around lines 52 - 55, Update
the response-handling branch in the test request flow so any completed fetch
with res.ok false increments failures and is reported as a test failure. Reserve
the existing skip/return-null behavior only for transport or timeout errors
caught from fetch, ensuring reachable 404 and 5xx responses cannot allow the
guard to pass.

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)`)
Comment on lines +79 to +88

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

Validate the required H2-to-H3 footer sequence.

cardH2s counts every H2 in the document. It does not require any H3 footer column headings. A page with one H1 and four unrelated H2 elements passes these checks if it has no level jump. Require an H3 band after the H2 band and reject H2 elements after that band.

Proposed fix
-  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`)
+  const firstH3 = levels.indexOf(3)
+  ok(
+    firstH3 >= 5 &&
+      levels.slice(1, firstH3).every((level) => level === 2) &&
+      levels.slice(firstH3).every((level) => level >= 3),
+    `live ${name} page keeps the flat H2 band before the footer H3s`
+  )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)`)
const firstH3 = levels.indexOf(3)
ok(
firstH3 >= 5 &&
levels.slice(1, firstH3).every((level) => level === 2) &&
levels.slice(firstH3).every((level) => level >= 3),
`live ${name} page keeps the flat H2 band 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)`)
🤖 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-heading-hierarchy.mjs` around lines 69 - 78, Update
the heading validation around cardH2s and the levels loop to require a
contiguous footer H3 band immediately after the flat H2 band, and reject any H2
appearing after that H3 band. Keep the existing jump detection, but anchor the
checks to the ordered heading levels so unrelated H2 elements cannot satisfy the
requirement without the required H2-to-H3 footer sequence.

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"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Remove the contradictory CI-wiring assertions

After the live probe was removed from test and ci, this assertion still requires it to be present there. In this revision both package scripts omit test-public-live-heading-hierarchy.mjs, so npm run site:check-live-heading-hierarchy always records two failures and exits 1 even when every live response is correct; the new command therefore cannot serve as a successful post-deployment check.

Useful? React with 👍 / 👎.

"npm test runs the public live heading-hierarchy guard"
)
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)