test(ci): gate faq.html anchor uniqueness + JSON-LD script-breakout escaping - #172
Merged
Conversation
…scaping Adds ci/check-faq-rendering.py, wired into ci/run-fixtures.sh, as a permanent regression gate for two of typikon#92's 18 findings: - faq.html's auto-slugify anchor collision (two questions that slugify to the same base id must still render distinct element ids). - ld-organization.html's unescaped json_encode script-breakout (a field value containing a literal `</script>` must never terminate a JSON-LD <script> block early). Both are template-level defects; the fixes belong to PR #171 (fix/92-low-severity-batch), which is not touched here. PR #171's own verification of these two was manual and one-time; this PR adds the missing permanent CI coverage instead: the fixture content in examples/sample-shop/content/faq.md plus this check turn PR #171's manual "negative fixture" narrative into a standing gate. Because the fixture deliberately exercises both live bugs, this check (and the pre-existing csp-enforce + pa11y stages, which independently flag the same script-breakout and duplicate-id defects on the current, unfixed templates) will not go green until PR #171 merges. That is expected, not a defect in this change — verified against PR #171's templates in a scratch build outside this branch: the identical check and fixture pass clean, and the full `ci/run-fixtures.sh` pipeline exits 0. The interrupted branch's schemas/page.schema.json edit is dropped as obsolete: main split that file into page.schema.json + a new page.core.schema.json since the interrupted branch's base commit, and the og_image extension-allowlist fix it made now lives in page.core.schema.json, which PR #171 already carries.
forkwright
force-pushed
the
fix/92-faq-rendering-check
branch
from
August 17, 2026 15:05
a1f1c91 to
fd60f4c
Compare
…nvoking it bare The derived fixture runner discovers ci/check-*.py by glob and invokes each with no arguments. check-faq-rendering.py requires a built sample-shop public/ dir, so the discovery call hit its usage line and exited 2, failing the gate: usage: ci/check-faq-rendering.py <built-sample-shop-public-dir> ##[error]Process completed with exit code 2. It is now declared in ARG_TAKING_FIXTURES and invoked explicitly after the build, the same shape check-xml-output.sh already uses. The WHY comment above that map now states the general rule rather than just listing members: a check needing BUILT output belongs there by construction, because discovery runs before any example is built. Bare invocation either fails on usage -- which is what happened here and is the loud outcome -- or inspects a directory that does not exist yet and reports a vacuous pass, which is the quiet one and worse. Verified against a fresh sample-shop build: exit 0, 'anchors unique, 5 checked; script-breakout escaped'. The stale build left over from before #171 merged gave an unplanned failure proof -- the check reported the exact anchor collision and unescaped </script> that #171 fixed, so it demonstrably fails on the state it exists to catch.
…right/typikon into fix/92-faq-rendering-check
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Salvages the genuinely non-duplicated portion of the interrupted branch
fix/92-audit-defects(two "wip: interrupted by an operator-requestedrestart" commits — cut off mid-implementation, never reviewed). Adds a
permanent CI regression gate,
ci/check-faq-rendering.py, for two oftypikon#92's 18 findings that PR #171 fixes at the template level but
does not gate with an automated check.
This complements PR #171 (
fix/92-low-severity-batch); it does notoverlap it. Comparing the interrupted branch's file set against #171's
diff, only four files were genuinely untouched by #171:
ci/check-faq-rendering.py,ci/run-fixtures.sh's wiring,examples/sample-shop/content/faq.md's fixture content, and an edit toschemas/page.schema.json. Everything else in the interrupted branchduplicated #171's work and was discarded — not ported, not merged.
What the check does
ci/check-faq-rendering.pyreads the already-builtexamples/sample-shop/public/faq/index.html(built earlier inci/run-fixtures.shbybin/typikon-check) and asserts two propertiesthe fixture is shaped to exercise:
worldwide?" / "Do you ship worldwide?!") auto-slugify to the same base
id. The rendered page must carry the base id plus a distinct
disambiguated sibling, and no
faq-itemid may repeat. The check doesnot hardcode the disambiguated suffix as a literal string — fix(bin,ci,schemas,templates): resolve 16 low-severity correctness and claim-accuracy defects #171's
actual disambiguation scheme suffixes with the colliding item's
absolute position in the questions array (
loop.index), not aper-collision counter, so a literal
-2expectation would have beenwrong the moment the fixture gained an earlier question. Asserted
structurally instead: base id present, at least one sibling id sharing
its
<base>-prefix.literal
</script>must render as inert text everywhere, includinginside the FAQPage
<script type="application/ld+json">block.Detection goes through
json.loads()on each extracted JSON-LD blockrather than a substring search for
</script>inside the block body:the block-extraction regex is itself non-greedy up to
</script>, soa genuine unescaped breakout is always consumed as the block's own
terminator and can never appear inside an already-extracted body
string — a raw substring check for it there is dead code that can
never fire on the exact input it exists to catch (confirmed while
writing this: the interrupted draft's original substring check never
triggers, even against the live unfixed bug). A truncated body is
instead syntactically invalid JSON, and
json.loads()catches thatdirectly with an actionable error.
Both properties were guessed at by the interrupted draft before #171's
actual implementation existed; both guesses needed correcting against
#171's real committed templates (the exact suffix scheme, and the dead
detection path above).
Which of #92's 18 findings this covers
Zero, newly. PR #171's own body states "18/18 accounted for" — all
18 of typikon#92's findings are already fixed, either by #171 itself or
by prior PRs it cites. This PR does not add a fix for anything still
open.
What it adds: permanent, automated CI coverage for 2 of those 18
(the
templates/faq.html:36auto-slugify collision, and thetemplates/partials/ld-organization.html:19JSON-LD script-breakout).PR #171 verifies both via one-time manual "negative fixture" runs
described in its own PR body prose — real, but not standing. This PR
turns that into an enforced
ci/run-fixtures.shstage so a futuretemplate change can't silently reintroduce either defect.
Using Refs #92, not a closing keyword, since this PR doesn't close
anything #171 hasn't already closed.
Dropped as obsolete
The interrupted branch also touched
schemas/page.schema.json(aone-line
og_imageextension-allowlist fix, adding.jpeg). Since thatbranch's base commit, main split
page.schema.jsonintopage.schema.json+ a newpage.core.schema.json(composed via$refunevaluatedProperties); theog_imagepattern now lives inpage.core.schema.json, and PR fix(bin,ci,schemas,templates): resolve 16 low-severity correctness and claim-accuracy defects #171 already fixes it there(
schemas/page.core.schema.jsonin fix(bin,ci,schemas,templates): resolve 16 low-severity correctness and claim-accuracy defects #171's diff). Resurrecting theinterrupted branch's edit would have reverted the split architecture on
a field that no longer lives in that file. Dropped, not ported.
Verification
Mutation test (fixture, not the checker) — watched it fail, then
confirmed a clean restore:
Baseline: PR #171's real templates (scratch build outside this branch,
not committed here) + this branch's fixture/checker/wiring →
check-faq-rendering: ok (anchors unique, 5 checked; script-breakout escaped), exit 0.Mutated
examples/sample-shop/content/faq.md's</script>question toplain text (same scratch build):
Exit 1, actionable. Restored the fixture, rebuilt: clean pass again,
exit 0. The same mutate → rebuild → confirm-RED → restore cycle was also
run directly in this branch's own worktree;
git diff -- examples/sample-shop/content/faq.mdafter the restore shows the identical 12-line addition as before the
mutation — a clean revert.
Clean pass on a correct implementation: confirmed above (exit 0)
against PR #171's actual committed templates in a scratch copy, and via
the full
ci/run-fixtures.shpipeline end-to-end (see below) — includingcsp-enforceandpa11y, both of which independently flag the same twolive defects on unfixed main (see below), also going clean.
Against this branch's own tree (current
main, no #171): the checkcorrectly goes RED — main has not merged #171 yet, so both defects are
genuinely still live:
This is expected, not a defect in this PR — it is the check correctly
detecting that main genuinely still has both bugs.
bash ci/run-fixtures.shfrom this branch (real exit code):REAL_EXIT=1. Two pre-existing stages independently corroborate thesame two live defects before the pipeline ever reaches this PR's new
check (the script uses
set -e, so it stops at the first failingcommand —
bin/typikon-check examples/sample-shop):csp-enforce— flags a stray inline<script>...</script>body onfaq/index.html:86, a direct side effect of the breakout truncatingthe FAQPage JSON-LD block early and desynchronizing the parser from
the real BreadcrumbList block after it.
pa11y(axe-core) — flagsDuplicate id attribute value "do-you-ship-worldwide"on the FAQ page — a real, independently-tooledaccessibility violation caused by the same un-deduplicated anchor.
Both disappear cleanly once PR #171's templates are applied (see the
full-pipeline run below), which is exactly the corroboration this PR
exists to lock in.
pa11y note:
pa11y-ciis installed on this box but~/.npm-global/binis not on
PATHby default — withPATH="$HOME/.npm-global/bin:$PATH"added for the run, pa11y ran for real rather than skipping (an
environment gap on this box, not a defect; not touched).
Full pipeline against PR #171's templates (scratch, not committed
here):
bash ci/run-fixtures.shwith the samePATHaddition —REAL_EXIT=0, every stage includingcsp-enforce,pa11y, and thisPR's new
check-faq-renderingstage passes clean end to end.kanon lint— zero violations on all three changed/added files(caught and fixed two
PYTHON/empty-fstringfindings in the checkerduring review).
Merge order
This PR's own CI will show red — not from a defect here, but because the
fixture deliberately exercises two bugs that are only fixed on
fix/92-low-severity-batch. Recommend merging #171 first; this PR thengoes green with no further changes.