Skip to content

48a09586 - Referral landing follow-ups from the #30 review - #32

Merged
TaprootFreak merged 8 commits into
developfrom
48a09586-referral-landing-followups
Sep 9, 2026
Merged

48a09586 - Referral landing follow-ups from the #30 review#32
TaprootFreak merged 8 commits into
developfrom
48a09586-referral-landing-followups

Conversation

@TaprootFreakAI

@TaprootFreakAI TaprootFreakAI commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

EN. Follow-up to #30, which was merged after a full double review. The five defects that could credit a referral to the wrong person were fixed there; this PR carries the two remaining user-visible ones — an English locale served with German copy, and a landing page that spins for ever without JavaScript — plus a third the review of this PR uncovered: the browser-side copy of the share helpers had drifted from the one production serves. Everything else the review found is listed below with its evidence, unfixed and deliberately so.

DE. Nachtrag zu #30, der nach vollständigem Doppelreview gemergt wurde. Die fünf Fehler, die eine Empfehlung dem Falschen hätten gutschreiben können, sind dort behoben; dieser PR bringt die zwei verbliebenen sichtbaren Mängel — englische Seite mit deutschem Text und eine Landingpage, die ohne JavaScript ewig lädt — plus einen dritten, den erst das Review dieses PR fand: die Browser-Kopie der Share-Helfer war von der produktiv ausgelieferten abgewichen. Alles Übrige aus dem Review steht unten mit Beleg, bewusst unbehoben.

What this PR changes

?lang=en without a code served German copy under an English locale.
injectShareLocaleHtml flips html lang and og:locale whenever lang=en, but shareTitle and shareDescription returned null without a code, so the shell's German <title> and description survived. Crawlers snapshot exactly those bytes. Both now fall back to generic English copy, worded identically to I18N.en so the crawler snapshot and the JS-rendered page do not disagree. German is unchanged, because German is the shell's language and there is nothing to replace. og:image:alt deliberately does not take the codeless title: alt text describes the picture, and without a code the picture is the generic og.png the shell already labels RealUnit.

Without JavaScript both landings sat in the loading state for ever.
Nothing resolves the code, and there was no <noscript>. Both shells now hide #state-loading from a <noscript> in the head — <style> is metadata content and is only conformant there — and explain the situation in both languages, since ?lang=en may already have flipped the document. The copy names only what is actually reachable without JavaScript: enable it and reload, or use the store links below. An earlier draft told the visitor to open the link in the app, but the control that does so (#ok-open) is hidden in that state.

The browser mirror had drifted from the module production serves.
public/js/lib/invite-core.js carries a second copy of shareTitle and shareDescription. Nothing in production calls that file's HTML injectors, so the divergence would not have failed a single test. Both helpers now match functions/lib/itunes-banner.js byte for byte, and a matrix test over kind × code × lang pins them against each other, so the next drift fails the suite instead of the next review. The injectors there are left alone on purpose: parseCodeFromLocation returns null without a code, so their codeless branch is unreachable and forcing parity would mean changing a parser the browser page actually runs. Both modules also state in a comment that their English fallback is the same string as I18N.en, which public/invite/invite.js reads at runtime; that claim is now pinned by a test too, so editing the catalogue alone cannot split the crawler snapshot from the rendered page.

The JS-less panel is now a view in the screenshot matrix.
Every view rendered with scripting on, so the matrix could not express the state this PR introduces and nothing would have caught the panel breaking. Views take an optional noJs flag, tests/visual.spec.mjs passes it into the browser context, and settle skips the font-readiness question when there is no page context to ask. The six baselines were rendered by the pinned container in CI, not on the arm64 host that would have produced different text metrics. The invite and promo baselines are byte-identical by design: without scripting both pages show the same panel, and holding them to the same image is what keeps the two from drifting.

Verification
  • npm run check (format, HTML validation, check:site, unit tests + coverage): exit 0, 162 tests, thresholds met.
  • npx playwright test behavior.spec.mjs --project=desktop-chromium: 101 passed, 1 skipped.
  • CI at this head: Quality and Screenshots both green, including check:visual, verified per check-run against the head SHA rather than against a branch name.
  • Every guard here was verified by mutation, not by assertion alone. Each of these turns exactly one test red: noscript section { display: none }; hiding only the English paragraph; removing the <noscript> wrapper; shortening the German copy; letting either share helper drift from the function module; editing I18N.en['doc.desc'] without touching the fallback literals.
  • The no-JS case asserts visibility, not the served bytes. Measured on this Playwright version with javaScriptEnabled: false: role and CSS locators do reach into <noscript>, getByText does not — its text engine skips that subtree. An earlier revision of this PR asserted on page.content(), which a display rule would have left green while the page stayed blank.
Found and deliberately not fixed — needs a decision or a deploy to settle

Cannot be settled from this repo — one curl after deploy will:

  • Whether _redirects and _headers apply to requests a Pages Function serves. If they do not, /invite/AB12CD 404s and the CSP is absent on exactly the pages that call fetch. Evidence against: _middleware.js calls context.next() rather than building its own response, and copies the response headers; production demonstrably serves CSP and X-Frame-Options today. There is no preview deployment for the PR, so this stayed unverified. curl -sI https://realunit.app/invite/AB12CD after the next deploy answers it.
  • public/_redirects:7-13 uses absolute URLs (https://www.realunit.app/…) as redirect sources. Cloudflare Pages expects paths there; scripts/check-site.mjs:617-641 actively requires these lines, so the check pins a configuration that may never fire. Affects the www AASA/assetlinks guarantee in README.md:35-36.

Dead weight worth a scope decision:

  • Ten HTML injectors in public/js/lib/invite-core.js have no production caller at all; only applyItunesBannerFromLocation does. They are a second implementation kept alive by tests, and they are the reason 01a03535 - Add invite and promo landing pages #30's coverage badge measured the wrong file. Deleting them is the cleaner answer than mirroring them forever, but it is a scope call for the owner, not a follow-up fix.

Gates that pass on shape rather than behaviour:

  • scripts/check-site.mjs:351-395 asserts that strings appear in a file. A middleware that imports the injector but never calls it, or an injector returning the input unchanged, passes. It did not catch anything in this feature.
  • scripts/check-visual.mjs:77-91 compares aggregate counts. A test without a toHaveScreenshot() stays expected and the total still matches. Pre-existing, but 01a03535 - Add invite and promo landing pages #30 grew the matrix by 115 baselines.
  • The iPad app-card tags are emitted (functions/lib/itunes-banner.js) and asserted nowhere; check:site checks only iphone and googleplay, though CONTRIBUTING.md:21-22 requires twitter:app:url:ipad. They could be deleted and all four gates would stay green.

Behaviour worth a product decision:

  • capCode is not idempotent, and the path and query forms decode a different number of times: /invite/%2541 yields %41 while ?code=%2541 yields A. The same transport-encoded value therefore resolves to two different codes depending on how the link is written.
  • The iOS CTA opens realunit-wallet://… with no store fallback, so iOS without the app installed is a dead end.
  • scripts/dev-server.mjs:97-107 hardcodes the invite/promo rewrite, so the whole Playwright suite exercises a different routing mechanism than production.

Test coverage still missing (each verified absent): duplicate ?code=A&code=B in both orders; the idempotency property normalize(normalize(x)) === normalize(x); host and scheme variants (realunit.app.attacker.example, https://realunit.app@attacker.example, //attacker.example, javascript:, data:, punycode); code separators other than the slash; boundary lengths 31/32/33 and 10 KB; the app-not-installed hand-off.

Two follow-ups from the review of #30.

?lang=en without a code flipped html lang and og:locale to English while
the shell text stayed German — crawlers snapshot exactly those bytes.
shareTitle and shareDescription now fall back to generic English copy.

Without JavaScript the loading state sat there for ever: nothing resolves
the code. Both shells now hide it and say why, and a Playwright case with
javaScriptEnabled:false asserts the served bytes.
The no-JS hint was German only — the same defect this PR fixes, since
?lang=en flips the document to English. It is bilingual now, and its
<style> moved into a head <noscript> where the spec allows it.

The English fallback said "your code" where I18N.en says "this code", so
the crawler snapshot and the JS-rendered page disagreed. The kind guard in
shareTitle went back to the front: reordering it made shareTitle(null, null,
'en') render as an invitation.

Adds the end-to-end assertion the helper tests were missing, and corrects
the Playwright comment: getByText genuinely does not reach into <noscript>
in this version, which is why the assertion reads page.content().
The follow-up review found the codeless English fallback only on the
function module: public/js/lib/invite-core.js kept its own shareTitle and
shareDescription and now answered a codeless ?lang=en landing differently.
Nothing in production calls that file's HTML injectors, so no other test
would have gone red. Both helpers now match the function module byte for
byte, and a matrix test pins them against each other so the next drift
fails the suite instead of the next review.

The injectors there stay as they are: parseCodeFromLocation returns null
without a code, so their codeless branch is unreachable and forcing parity
would mean changing a parser the browser page actually runs.

Three smaller points from the same review:

- og:image:alt described the picture, so the codeless English title no
  longer lands there. Without a code the image is the generic og.png the
  shell already labels "RealUnit".
- The JS-less hint told the visitor to open the link in the app, but the
  only control that does so is hidden without JavaScript. It now names
  what is actually reachable: enable JavaScript and reload, or use the
  store links below.
- The no-JS test asserted on page.content(), which a rule like
  "noscript section { display: none }" would leave green while the page
  stayed blank. It now asserts visibility. Measured on this Playwright
  version: role and CSS locators do reach into <noscript>, getByText does
  not, so the earlier comment was too broad and is corrected.

npm run check: exit 0, 161 tests, coverage thresholds met.
playwright behavior.spec.mjs --project=desktop-chromium: 100 passed,
1 skipped. Both new guards verified by mutation: hiding the noscript
section and letting the mirror drift each turn their test red.
…full text

The logic lane found the notice only half guarded. The German paragraph was
asserted by its heading alone, and nothing anywhere checked that the notice
stays inside <noscript>: dropping that wrapper would show every visitor a
warning about JavaScript being off, and the existing case runs with scripting
disabled, where the wrapper's children are parsed as ordinary markup either
way — so it would have stayed green.

Both paragraphs are now asserted by their full text, the lang attributes are
pinned, and a second case with JavaScript enabled requires the wrapper to be
present and the heading to be absent.

Verified by mutation: removing the <noscript> wrapper turns the new case red,
and shortening the German copy turns the existing one red.

npm run check: exit 0, 161 tests. playwright behavior.spec.mjs
--project=desktop-chromium: 101 passed, 1 skipped.
…sent

toHaveText matches textContent, which display:none leaves untouched, so the
previous revision would have stayed green on a rule that hid either paragraph
while the visitor saw nothing. The heading kept its toBeVisible check, so the
whole-section case was still caught; a rule aimed at one paragraph was not.
Both paragraphs now assert visibility before their text.
…reuse

Both modules say in a comment that their codeless English copy is the same
string as I18N.en, so the crawler snapshot and the JS-rendered page cannot
disagree. Until now the tests pinned hand-written literals on both sides, so
editing the catalogue alone — which public/invite/invite.js reads at runtime —
would have split the two while the whole suite stayed green.

Verified by mutation: changing only I18N.en['doc.desc'] turns the new test red
and nothing else.
The conformity lane asked for this and it is the right call. Both landings now
have a visible state this PR introduces — the <noscript> panel — and the visual
matrix could not express it: every view renders with scripting on, so nothing
would have caught the panel breaking, and CONTRIBUTING listed variants that no
longer matched the matrix.

Views get an optional noJs flag, visual.spec.mjs passes it into the browser
context, and settle skips the font-readiness question when there is no page
context to ask. Baselines are generated by the pinned container in CI, not on
this host, so the committed PNGs match what the gate compares against.
Six PNGs, two views across three viewports, taken from the CI run at 78977e1
that failed with "a snapshot doesn't exist". Generating them on this host would
have produced arm64 macOS text rendering and turned the gate red against the
amd64 Linux container it actually compares in.

The invite and promo baselines are byte-identical by design: without scripting
both pages show the same panel, and holding them to the same image is what
keeps the two from drifting apart.
@TaprootFreakAI

Copy link
Copy Markdown
Collaborator Author

EN. Four review verdicts on this head, two vendors × two dimensions, all at STATUS: complete and zero findings. Getting there took four rounds and five findings — every one of them against code written in this PR, and every one now covered by a test that fails when the fix is reverted. CI is green at this exact SHA, checked per check-run rather than by branch name.

DE. Vier Prüfurteile auf diesem Stand, zwei Anbieter × zwei Dimensionen, alle vollständig und ohne Befund. Dahin waren es vier Runden mit fünf Funden — allesamt an Code aus diesem PR, und jeder ist jetzt durch einen Test abgesichert, der ohne die Korrektur rot wird. Die CI ist an genau diesem Commit grün, geprüft je Check-Lauf statt über den Branch-Namen.

Rounds and findings
Head Conformity Logic Finding
1548802 codex 0 codex 1 The <noscript> panel was only half guarded: the German paragraph rode on the heading assertion, and nothing checked that the panel stays inside <noscript> — dropping the wrapper would have shown every visitor a warning about JavaScript being off, with no test going red.
ea10c50 codex 1, grok 0 codex 0, grok 1 Both lanes, independently: toHaveText matches textContent, which display: none leaves untouched. A rule hiding one paragraph would have stayed green.
8105a08 codex 1, grok 0 codex 0, grok 0 The new panel is a visible page state that the screenshot matrix could not express, because every view renders with scripting on.
d29f784 codex 0, grok 0 codex 0, grok 0

An earlier round also caught a copy divergence the tests could not see: both modules claim in a comment to reuse I18N.en, but the tests pinned hand-written literals on both sides, so editing the catalogue alone — which public/invite/invite.js reads at runtime — would have split the crawler snapshot from the rendered page while the suite stayed green.

Reviews ran read-only against a frozen checkout at the exact SHA, after one lane correctly reported that it had been reading a working tree while a commit landed underneath it.

Every guard verified by mutation

Each of these turns exactly one test red, measured at the committed SHA rather than on a working copy:

  • noscript section { display: none }
  • hiding only the English paragraph
  • removing the <noscript> wrapper
  • shortening the German copy
  • letting either share helper drift from the function module
  • editing I18N.en['doc.desc'] without touching the fallback literals
One correction worth recording

An earlier revision of this PR reported a green local run while CI was red at the same head. The test asserted a string that had never existed in the shipped HTML: the test host carried an older wording than the commit, so the green number belonged to bytes that were never pushed. That is the same class of false claim this PR series was opened to fix. Verification now runs against a checkout of the committed SHA, and CI is read per check-run at that SHA.

@TaprootFreakAI
TaprootFreakAI marked this pull request as ready for review September 9, 2026 02:54
@TaprootFreak
TaprootFreak merged commit 2265935 into develop Sep 9, 2026
2 checks passed
TaprootFreak pushed a commit that referenced this pull request Sep 9, 2026
* fix(invite): report a rewritten landing as found, not as 404

Measured against the deploy, which is what #30 left open and #32 promised to
settle with one curl:

  curl -sI https://realunit.app/invite/AB12CD  ->  HTTP/2 404

The body is correct — the landing shell with the rewritten meta tags — but the
status is not. _routes.json sends /invite/<code> to the Function, and the asset
lookup behind context.next() resolves the code-less shell through the
_redirects 200-rewrite while keeping the not-found status of the path that was
asked for. The middleware copied that status through.

Browsers render a 404 body, so a person following the link sees the right page.
Share crawlers do not: they drop a 404 before reading the tags the middleware
just wrote, which is the entire reason the rewrite exists. Every invite and
promo link shared today therefore previews as nothing.

The promotion is guarded on the landing marker, so a broken deploy that serves
the site's own 404 page on these paths keeps saying 404 instead of looking
healthy, and the reason phrase is dropped rather than left reading "Not Found".

functions/_middleware.js was outside coverage while it decided the status every
crawler sees, which is how this shipped unnoticed. It is measured now, at 100%,
with the entry point's own test file.

npm run check: exit 0, 173 tests. Verified by mutation: reverting the promotion
turns three tests red, and removing the marker guard turns the two that protect
a genuine 404 red.

The other half of the same open question is now answered too: _headers do apply
to Function-served requests. CSP, X-Frame-Options and Referrer-Policy are all
present on https://realunit.app/invite/AB12CD.

* docs(invite): describe the status promotion where the rules are written

The conformity lane found the documentation and the config describing a
narrower change than the one that landed, which is the same drift #30 had to
correct in this file.

- CONTRIBUTING said public/js/lib was the only code with a coverage gate and
  the gate table named only that path; functions/lib and the Function entry
  point carry one too.
- The ground rule described the middleware as a pure byte rewrite with
  "nothing else transformed", which no longer holds now that it also decides
  the status a crawler sees.
- The vitest comment still claimed the measured surface was side-effect-free
  browser logic only.
- The file header of the middleware did not mention the promotion at all.

Two lanes independently found the same test gap: the promoted responses
asserted their status but not their reason phrase, so reverting the ternary
would have stayed green at 100% branch coverage. Both promotion tests now
assert the empty phrase, and the untouched-404 test asserts it keeps
"Not Found".

npm run check: exit 0, 173 tests, functions/_middleware.js at 100%. Verified by
mutation: keeping the reason phrase on promotion turns the two promotion tests
red.

* test(invite): anchor the status promotion to the shipped pages

The conformity lane found the middleware test building its own landing shell
and its own 404 page. The promotion keys on a marker that has to be in the
landings and must never be in the site's 404 page, and a synthetic fixture
keeps passing after someone moves that marker — production would answer 404
again with nothing going red, which is exactly how this shipped.

The test now reads public/invite/index.html, public/promo/index.html and
public/404.html, and asserts the marker contract against them directly.

npm run check: exit 0, 174 tests. Verified by mutation on the real files:
renaming the marker in the landing turns two tests red, and adding it to the
404 page turns two others red.

* fix(invite): answer HEAD like GET, and prove the headers survive

The logic lane caught the fix stopping at GET. That is the method this bug was
measured with: `curl -sI` sends HEAD, so the very command quoted as the proof
would still have read 404 after the fix, and a link checker that sends HEAD
first would still see the link as dead. HEAD has to answer with the same status
as GET.

HEAD is now handled alongside GET and returns no body of its own. A HEAD whose
body the platform withheld keeps the status it came with: without a body there
is no way to tell the landing shell from the site's own 404 page, and guessing
from the path alone would make a broken deploy look healthy.

The same lane found the header pass-through untested. The fixture now carries
the Cache-Control rule public/_headers sets for these paths, and the promotion
test asserts it survives the response being rebuilt.

Also recorded in the marker comment: two other pages carry the same id. It does
not matter, because the caller has already established the path is a landing,
so the only open question is shell versus 404 page.

npm run check: exit 0, 176 tests, functions/_middleware.js at 100%. Verified by
mutation: dropping HEAD from the guard, returning a body for HEAD, and keeping
content-length instead of cache-control each turn one test red.

* test(invite): assert the HEAD response's headers and its absent body

The logic lane found the HEAD path asserting only status, reason phrase and an
empty text(). Two regressions would have stayed green: keeping content-length
for HEAD alone, and returning an empty body instead of no body, which text()
cannot tell apart.

The HEAD case now asserts body === null and the same three headers the GET case
does.

npm run check: exit 0, 176 tests. Verified by mutation: an empty body for HEAD
and a HEAD-only content-length each turn one test red.

* docs(invite): carry the status promotion into the README too

The conformity lane found README describing the middleware as byte injection
only, and its testing section naming public/js/lib as the sole measured
surface. CONTRIBUTING names README as the architecture reference, so leaving it
behind recreates the drift the last PR had to correct.

Both passages now match what the code does.

* fix(invite): decide the HEAD status from a GET-equivalent lookup

The logic lane found the HEAD path resting on an assumption instead of a fact.
The status has to be decided from the body, because the marker in it is what
tells the landing shell from the site's own 404 page — and a HEAD response has
no body. The previous revision kept the status when the body was missing, so if
the platform hands the Function nothing for HEAD, the same link would have read
as found by GET and as dead by the HEAD a link checker sends first. That
contradicted the comment right above it.

HEAD now asks for the GET-equivalent through context.next() and answers with
that status and those headers, without a body. Nothing is left to the platform,
and the outcome no longer waits on a deploy to be known.

The same lane found the fixture carrying no security headers, so a middleware
that dropped one would not have failed a value assertion anywhere. The fixture
now carries what public/_headers sets for these paths, and both the GET and the
HEAD case assert every one of them survives.

npm run check: exit 0, 176 tests, functions/_middleware.js at 100%. Verified by
mutation: dropping the GET-equivalent lookup, deleting a security header, and
returning the GET body to a non-HTML HEAD each turn one test red.

* docs(invite): make the comments say what is actually true

Four accuracy findings from the conformity lane, all of them places where a
comment claimed more than it should:

- The "Browser JS and unit coverage" section still read as if everything
  outside public/js/lib were covered by Playwright alone. Two surfaces carry
  their own gate and are not page scripts at all.
- The test fixture called its Content-Security-Policy value "what
  public/_headers sets". It is a short stand-in for a much longer policy; only
  the cache-control value is the real one.
- "The contract the two tests above rely on" sat above two tests that do not
  use the marker at all.
- "the tags written just above" was copied out of the middleware header, where
  it had a referent. In the test it had none.

npm run check: exit 0, 176 tests.

* test(invite): take the header values from public/_headers, and cover HEAD on a 404

Two low findings from the conformity lane.

The fixture hand-copied a shortened Content-Security-Policy while calling it
what public/_headers sets. It now reads the real values out of that file, so
the fixture cannot drift from production and the pass-through assertion is
always about the shipped policy. Measured, and written into the comment so the
next reader is not misled: this checks the pass-through, not the policy —
changing _headers moves both sides together by design, and a middleware that
drops a header is what turns red.

The marker guard was only exercised on GET. There is now a HEAD case against
the real 404 page, so a regression that evaluates the guard for GET alone has
somewhere to fail.

npm run check: exit 0, 177 tests. Verified by mutation: deleting referrer-policy
in the middleware and weakening the marker guard each turn a test red.

* fix(invite): strip Range from the GET-equivalent a HEAD triggers

The logic lane found the synthetic GET inheriting every header of the HEAD it
came from, Range and If-Range included. A HEAD carrying Range would have come
back as 206 Partial Content, whose body is not the landing shell: the status
would neither have been promoted nor meant what the client asked for.

Both headers are now dropped and everything else the client sent is kept. The
test asserts exactly that, so a regression that forwards Range, or one that
throws the client's headers away altogether, has somewhere to fail.

The same lane found the non-HTML HEAD branch asserting status and body but not
headers, though it rebuilds the response too. It now checks them.

npm run check: exit 0, 178 tests, functions/_middleware.js at 100%. Verified by
mutation: keeping Range, dropping the client headers, and building the
body-less response with empty headers each turn one test red.

* docs(invite): stop repeating a phrase that has no referent

The conformity lane caught me reintroducing, in the middleware header, the
exact wording I had just corrected in the test file: "the tags written just
above", where nothing is above. It now says what it means.

The three places that mention the functions/lib ratchet now say where it is
set, instead of calling it documented and leaving the reader to find it.

npm run check: exit 0, 178 tests.

* test(invite): pin the promotion to 404 and nothing near it

The logic lane found that widening the condition from 404 to the whole 4xx
range would have passed the entire suite. Only the 404 Pages produces for these
paths is an artefact; a 403 or a 410 means what it says, even when the body
happens to be the shell — promoting those would turn a refusal or a withdrawn
code into a healthy page.

landingStatus now pins 403, 410 and 451, and the middleware has a case that
sends a refusal through untouched.

npm run check: exit 0, 179 tests. Verified by mutation: widening the condition
to `status >= 400 && status < 500` turns both new tests red.

* fix(invite): drop Range for a plain GET too, not only for HEAD

The logic lane found the previous commit cleaning the request on one path only.
A real GET carrying Range would still have been forwarded with it, and this
pass rewrites the whole document: a partial representation would be injected
into a fragment and returned under a Content-Range describing the bytes before
the rewrite.

Both methods now ask for the same thing — the whole document as a GET — so the
two paths share one helper and one comment instead of drifting apart again.

Measured against the deploy before changing anything:

  curl -sI -H 'Range: bytes=0-99' https://realunit.app/invite/AB12CD
  -> 404, and the body is the full 4162 bytes, not a fragment

Pages answers a Range request on these paths with the whole document today, so
this is a guard rather than a live fix, and the comment says so.

npm run check: exit 0, 180 tests, functions/_middleware.js at 100%. Verified by
mutation: cleaning only the HEAD path, and keeping Range at all, each turn tests
red.

* fix(invite): drop the headers that described the bytes before the rewrite

The logic lane found ETag and Last-Modified passed through unchanged. They
describe the document the asset server produced, not the one this pass sends,
and a stale validator is worse than none: a conditional request would be
answered 304 against a document the client never received. Content-Range gets
the same treatment for the same reason.

Measured against the deploy before changing anything: Pages sets neither ETag
nor Last-Modified on these paths today, so this is a guard rather than a live
fix, and the comment says so.

The refusal case now also asserts the body passes through, not just the status.

The conformity lane found the Range behaviour missing from both architecture
documents; CONTRIBUTING and README now say that GET and HEAD are resolved
internally as one full GET. The measurement note in the code also says what was
actually measured — the body of a Range GET, not only its headers, which is
what the previous commit message implied.

npm run check: exit 0, 181 tests, functions/_middleware.js at 100%. Verified by
mutation: dropping only content-length, and building the header set empty, each
turn tests red.

* fix(invite): make the GET-equivalent unconditional, and never pair a body with 304

The logic lane found the helper promising more than it delivered. It ruled out
206 by dropping Range, but forwarded the conditional headers untouched, so the
answer could still come back as 304 or 412 — bodies that are not the landing
shell either, on a path where the status is decided from the body.

Worse, a 304 would have crashed the request: the previous revision paired the
rewritten string with that status, and constructing a Response with a body and
a null-body status throws. A conditional request would have become a 500
instead of being passed on.

Both are closed. Every header that could make the answer something other than
the full document is dropped, and 204, 205 and 304 are answered without a body.

The same lane found that re-methoding a request that carries a body throws,
which would have aborted the middleware rather than answering. That case now
falls back to rebuilding from the URL, which loses the platform's request
metadata but keeps the request answerable, and a test drives it.

npm run check: exit 0, 184 tests, functions/_middleware.js at 100%. Verified by
mutation: narrowing the header list back to Range, treating only HEAD as
body-less, and removing the fallback each turn a test red.

* fix(invite): drop the stale Content-Encoding, and stop saying "everything"

The logic lane left this as an open question rather than a claim, and the
measurement settled it:

  curl -sSI --compressed https://realunit.app/invite/AB12CD
  -> content-encoding: gzip

These paths really are served gzipped. The rewrite always hands on a decoded
string, so a Content-Encoding inherited from the asset response would label it
as something it is not, and a body labelled gzip that is not gzip does not
render at all. Whether the header is on the object the Function sees or only
added by the edge afterwards is not knowable from here — dropping it costs
nothing in the second case and prevents a blank page in the first.

The conformity lane found the comment above that list, and the test title,
saying "everything that described the bytes" while the code covers a named set.
That is the same overclaim this series has now corrected four times. Both name
the five headers instead, and the helper's comment names the six request
headers RFC 9110 defines rather than claiming to cover every possible one.

npm run check: exit 0, 184 tests, functions/_middleware.js at 100%. Verified by
mutation: keeping Content-Encoding turns the test red.

* docs(invite): stop crediting the dev-server with the middleware's behaviour

The conformity lane found the README attributing the status promotion, the
HEAD parity and the unconditional full GET to both injection paths, because
the sentence followed a bracket naming the Function and the dev-server
together. scripts/dev-server.mjs has none of it.

While correcting that I checked the file rather than trusting the finding, and
the first correction I wrote was wrong too: the dev-server does answer 404, on
its own not-found path. What is true is narrower — it shares the injection and
answers HEAD without a body, but has no promotion, no marker guard and no
header stripping, because its routing serves the landings as 200 and never
produces the not-found status the promotion exists to correct. Both documents
now say that, and both name the conditional validators the helper strips
alongside Range.

The logic lane added two small ones, both taken:

- The integrity digests of RFC 9530 and its predecessors describe the bytes
  this pass replaces, so they join the stale list. Not observed on this deploy;
  the same guard as the validators.
- The body-carrying-GET fallback asserted the method and URL but not the
  headers, so rebuilding it with the client's untouched headers would have
  stayed green. It now checks that the cleaned set is what gets forwarded.

npm run check: exit 0, 184 tests. Verified by mutation: dropping a digest from
the list, and forwarding the raw headers in the fallback, each turn a test red.

* docs(invite): say what the rewrite invalidates, not what the headers are

Two precision findings from the conformity lane, both the same reflex this
series keeps catching: a comment claiming a little more than the code holds.

- The stale-header comment called ETag and Last-Modified byte descriptions.
  They are validators of the selected representation, and a weak ETag
  explicitly does not promise byte equality. The reason they go is that
  rewriting the representation invalidates them, which is what it now says.
- A test was titled "keeps the request headers but drops Range" while the
  helper drops four conditional headers as well; its comment said everything
  else is kept. The title and comment now describe that case, and point at the
  case that covers the conditional ones.

npm run check: exit 0, 184 tests.

* fix(invite): declare UTF-8, refuse to rewrite a partial answer, clean the fallback

Four findings from the logic lane, after it downgraded two of its own and
rejected two more as unsupported.

- response.text() decodes as UTF-8 and a string body is encoded as UTF-8, so
  an origin charset carried through would have been a lie. The rewritten
  answer now declares text/html; charset=utf-8 itself.
- A 206 cannot be rewritten: the body is a fragment and the range metadata
  would stop matching it. Range is stripped before the lookup, so this only
  fires for an origin that answers 206 unasked, but the guard is one line.
- The body-carrying-GET fallback kept the headers that described that body, so
  the rebuilt request announced a body it does not carry.
- The non-HTML branch deliberately drops only content-length, because nothing
  was rewritten there and the validators still hold. No test showed that, so a
  regression widening it would have passed. It is asserted now.

The comment on the non-HTML branch also says what it previously left out: the
request has already lost Range and the conditional headers by then, which is
harmless only because these paths serve HTML alone.

While verifying, one of my own mutation checks came back green and I chased it
rather than accepting it. The check had silently not applied — the script's
output was piped away, so "pattern not found" was invisible, and the two
assertions it was meant to test had landed in the wrong test because the same
line occurs twice in the file. Both are corrected, and the check now fails as
it should.

npm run check: exit 0, 186 tests, functions/_middleware.js at 100%. Verified by
mutation: dropping the charset, removing the partial-answer guard, keeping the
body headers in the fallback, and widening the non-HTML strip each turn a test
red.

* docs(invite): separate the two claims about Content-Encoding, and ask instead of catch

Three findings from the conformity lane.

The comment on the stale-header list argued in one breath that dropping
Content-Encoding is "not hypothetical" because the deploy serves gzip, and that
it "costs nothing if it was never on the object". Those are two different
layers, and put together they contradict each other. Split: the encoding goes
because response.text() decoded the body, whatever the origin declared; whether
the header was ever on the object is a separate question, and the gzip seen on
the deploy is applied by the edge after this Function runs. Only the ETag and
Last-Modified sentence rests on a measurement.

A test titled "passed through untouched" checked status and body but not
headers, while its two siblings with the same word do. It checks them now.

The fallback used a bare catch, so any constructor failure would have been
rerouted through it while the comment named one cause. It asks whether the
request has a body instead, and anything else still surfaces.

npm run check: exit 0, 186 tests, functions/_middleware.js at 100%. Verified by
mutation: mangling a header on the non-HTML branch, and taking the re-method
path unconditionally, each turn a test red.

* fix(invite): let a body-less answer keep the length it correctly had

The conformity lane pointed out that the helper's own name for what it drops
was wrong. On the paths that reach it nothing was rewritten, so the origin's
Content-Length describes exactly the representation a GET would have returned
— RFC 9110 wants a HEAD to carry it. Calling it stale and deleting it lost
correct information.

It is kept now, and the tests assert it on both branches that use the helper.

The asFullGet docstring also claimed everything else the client sent is kept,
while the fallback right below it drops the body, the headers that described it
and the request metadata a URL cannot rebuild. It names that exception.

npm run check: exit 0, 186 tests, functions/_middleware.js at 100%. Verified by
mutation: deleting the length again turns two tests red.

* fix(invite): hand on what this pass has no business rewriting

Three findings from the logic lane.

A 304 was going through the rewrite branch, where its ETag was stripped and a
Content-Type set. That is the one validator a 304 has to keep — it is what the
client was matched on. 204, 205, 304 and 206 are now one set of statuses handed
on as they came: none of them carries a representation this pass could rewrite,
and their headers still describe what the origin meant. The special case that
kept a body away from them disappears with it.

The rewrite is also gated on the body being a landing shell. On a broken deploy
serving the site's own 404 page under these paths, the status was already left
alone, but the page's title was still rewritten into an invitation — a
description of something the visitor is not looking at.

The stale-header strip runs for every rewritten answer, not only a promoted
one, but only the promotion test proved it. Coupling the strip to the status
would have left a 403 or 410 carrying a length from before the rewrite, and no
test would have noticed. The refusal case asserts it now.

npm run check: exit 0, 186 tests, functions/_middleware.js at 100%. Verified by
mutation: coupling the strip to 404, shrinking the pass-through set back to
206, and removing the shell guard each turn a test red.

* fix(invite): read the media type as a media type, and leave other encodings alone

Two production findings from the logic lane, and two test gaps.

The content type was matched with includes('text/html'). That is
case-sensitive, so an origin answering Text/HTML would have been treated as
something else and never rewritten; and it is a substring test, so
application/json; profile="text/html" would have been read as HTML and had an
invitation injected into it. It is parsed as a media type now.

The charset was ignored. response.text() decodes as UTF-8 whatever the header
says, so a body in another encoding would have come back as replacement
characters and left here labelled UTF-8. Such an answer is handed on untouched
instead. The old test asserted the opposite — that an ISO-8859-1 answer gets
relabelled — and it went red, correctly; it now covers what the rewrite really
guarantees, which is that a rewritten answer says UTF-8 even when the origin
left it out.

The two test gaps: the partial-answer case asserted only status and body, and
the non-HTML pass-through named a few headers by hand. The first now uses the
landing shell as its fixture, so the guard has something to protect and a
mutation that lets HEAD through goes red; the second compares the whole header
set, so a regression that drops one nobody thought to name is caught.

npm run check: exit 0, 188 tests, functions/_middleware.js at 100%. Verified by
mutation: the substring match, the missing charset guard, letting HEAD past the
partial guard, and dropping a header in the body-less branch each turn a test
red.

* fix(invite): hand an untouched answer on as it came, not rebuilt from its text

Seven findings across both conformity and logic lanes.

The branch that decides not to rewrite was rebuilding the response from the
decoded string while keeping the origin's headers — the same mismatch the
rewrite branch strips headers to avoid, just without the stripping. It reads
from a clone now, so an answer this pass declines to touch is handed on exactly
as it came, body and headers both.

Accept-Ranges joins the stale list: after a rewrite there are no ranges of the
original bytes left to offer.

The docblock describing the status promotion had ended up above isLandingShell
when that predicate was extracted. It sits above landingStatus, which is what
it describes.

Four test gaps, three of them the same shape — asserting that a header is
absent afterwards while the fixture never set it, which proves nothing:

- the refusal case now sends a real Content-Encoding before checking it is gone;
- the stale-header case now sends a real Accept-Ranges;
- the media-type lookalike now carries the landing shell as its body, so a
  substring match would actually rewrite it and be caught;
- the two remaining "passed through untouched" cases assert the body, as their
  siblings already did.

Charset parsing is exercised across the shapes it arrives in: quoted, unspaced,
differently cased, and two encodings that must be handed on instead.

npm run check: exit 0, 189 tests, functions/_middleware.js at 100%. Verified by
mutation: reading without the clone, keeping Accept-Ranges, stripping only on a
promoted status, and matching the media type as a substring each turn a test
red.

* fix(invite): read the Content-Type parameters with quotes respected

The logic lane found the charset regex not quote-aware. A quoted parameter
value may contain a semicolon, so `text/html; foo="x;charset=utf-8";
charset=iso-8859-1` matched the charset embedded in the quoted value and missed
the real one behind it — the answer would have been decoded as UTF-8, rewritten
and relabelled, which is exactly what the charset check exists to prevent.

The parameters are split with quotes respected now, and every charset present
has to be UTF-8: two that disagree mean hands off rather than picking one.

The 206 fixture described a range that could not exist — `bytes 0-9826/4162`,
because .length counts UTF-16 units while a Content-Length counts bytes, and
the total had been left over from an earlier fixture. It uses real byte counts.

npm run check: exit 0, 189 tests, functions/_middleware.js at 100%. Verified by
mutation: dropping quote-awareness, taking the first charset instead of
requiring all, and leaving the quotes on the value each turn a test red.

* test(invite): make the untouched-404 cases tell the fix from the bug

The logic lane found both of them unable to distinguish the current code from
the one it replaced. Their fixture never set an encoding or a validator, and
the page's own bytes are the same either way, so rebuilding the answer from its
decoded text instead of handing on the origin's would have passed.

Both now receive an answer carrying Content-Encoding and an ETag, and require
the very object the origin returned — a rebuilt look-alike fails. The HEAD case
compares the whole header set, since only the body may be missing.

Two smaller ones from the conformity lane: a test titled for the answer being
the full document checked only the forwarded request, and now checks both; and
the two documents called the four request headers "conditional validators",
which is the RFC's name for ETag and Last-Modified, not for these.

npm run check: exit 0, 189 tests. Verified by mutation: rebuilding the untouched
answer from its text turns both cases red.

* test(invite): prove the foreign encoding survives, with real bytes

The conformity lane found the test claiming more than it showed. Its body was
the same JS string every other fixture uses, and a string body is always
encoded as UTF-8 — so it only ever proved that the guard flips on a foreign
charset label, never that a body in another encoding comes through intact.

It now sends real ISO-8859-1 bytes, with 0xE9 for 'é', which is not valid UTF-8
at all, and carries the landing marker so that nothing but the charset guard
stands between it and the rewrite. The answer is compared byte for byte.

Verified by mutation: removing the charset guard turns it red, because the byte
comes back as U+FFFD and the answer leaves as different bytes.

npm run check: exit 0, 189 tests.

* fix(invite): honour a quoted-pair when splitting the Content-Type

The logic lane found the parser toggling on every quote, escaped or not. RFC
9110 lets a backslash escape the next character inside a quoted value, so
`foo="a\";charset=utf-8"; charset=iso-8859-1` was split in the wrong place and
the wrong charset won. The escape is consumed now, and a quoted value has its
escapes resolved rather than only its outer quotes stripped.

Two test gaps from the same lane:

- The disagreeing-charset case was only tried one way round, so a change from
  "all must be UTF-8" to "the last one wins" would have passed. Both orders are
  covered.
- Two pass-through branches asserted their headers but not that the answer is
  the origin's own object, while the 404 branch beside them does. A rebuild at
  either would have gone unnoticed; both now require the same object and carry
  an encoding and a validator to prove it.

npm run check: exit 0, 189 tests, functions/_middleware.js at 100%. Verified by
mutation: dropping the escape handling, letting the last charset win, and
rebuilding the non-HTML answer each turn a test red.

* test(invite): count bytes as bytes, pin og:url, and finish the identity checks

Six findings from the conformity lane, all of them places where a comment or an
assertion was looser than what it stood next to.

- Two comments called a HEAD answer "handed on untouched" or "the same object".
  A body-less answer has to be a new object; only the GET path can be the
  origin's own. Both now say which is which.
- The fixture builder set Content-Length from .length while a comment three
  lines above explained that .length counts UTF-16 units and these pages carry
  multi-byte characters. Every fixture and assertion uses real byte counts now.
- An assertion checked for a URL string that og:url and twitter:url share, so a
  defect in the og:url replacement alone would have passed. It pins the whole
  tag.
- "The German shell must be left exactly as it is" was not true: the pass adds
  og:site_name regardless of language. The claim is narrowed to the copy, which
  is what the test checks.
- The last of the four pass-through branches still lacked the identity check its
  siblings got, and has it now.

npm run check: exit 0, 189 tests. Verified by mutation: breaking the og:url
replacement alone, and rebuilding the answer on the non-GET/HEAD branch, each
turn a test red.

* test(invite): pin the parameter name's case and the marker's exact shape

Two mutation gaps from the logic lane, which also found the quoted-pair bug
independently — that one was already fixed in 9ef5048.

- Every charset test used a lower-case parameter name, so dropping the
  case-folding would have passed while `Charset=` stopped being recognised.
  Both an upper-case UTF-8 and an upper-case foreign charset are covered.
- The landing marker is matched as a substring, so widening it from
  `id="state-loading"` to `state-loading` would have passed too — and a page
  that merely mentions the words would then have been promoted from 404 to 200
  and rewritten. A page that mentions them without the attribute is now a case.

npm run check: exit 0, 190 tests. Verified by mutation: removing the
case-folding and widening the marker each turn a test red.

* test(invite): require the origin's own answer on every pass-through branch

Two test-strength findings from the decisive round; both lanes reported nothing
that changes behaviour for a user or a crawler.

The already-found case asserted its status but not that the rewrite ran, so a
change that handed a 200 answer straight through would have passed. My first
attempt at fixing that was itself trivially true — it checked for a tag the
shipped shell already carries, and the mutation stayed green. Measured what the
rewrite actually changes on that URL: it normalises the canonical and the two
URL tags to the form without a trailing slash. That is what the test checks now.

The identity check that three pass-through branches carry was missing from the
other two, so a rebuild at either would have gone unnoticed. All five require
the origin's own object on the GET path.

npm run check: exit 0, 190 tests. Verified by mutation: skipping the rewrite on
an already-found answer, and rebuilding the body-less pass-through, each turn
tests red.

* docs(invite): state the precondition deviation instead of leaving it implicit

The decisive round came back with nothing behaviour-affecting from three of the
four lanes. The fourth raised two, and this is how they stand.

The first is real and is now written down where it happens rather than left for
the next reader to discover: stripping the conditional request headers means a
client sending `If-None-Match: *` gets 200 with the whole document where RFC
9110 §13.1 calls for 304, and `If-Match` never yields 412. These paths emit
neither ETag nor Last-Modified, so a client only reaches that case by sending a
precondition it was never given one for, and the answer it gets is the current
representation — never wrong, only larger than it had to be. Evaluating
preconditions properly means minting a validator for the rewritten document,
which is a larger change than this one and belongs in its own.

The second is not this PR's: shouldRewriteItunesBanner and parseLandingFromUrl
carry two different lists of asset extensions, so a referral code ending in
.json is skipped entirely while one ending in .html loses its code. Both
functions predate this change. It is written into the PR body as a follow-up.

Three test-strength points from the same round are closed: an unterminated
quoted charset, the reason phrase on a passed-on HEAD, and the boundary of the
marker check spelled out as a documented limit rather than left unstated.

npm run check: exit 0, 190 tests. Verified by mutation: dropping the
closing-quote check and blanking the reason phrase each turn a test red.

* test(invite): the last pass-through branch, and the third URL tag

Two test-strength points from the confirmation round; it reported nothing that
changes behaviour.

The branch for a path the rewrite does not own was the one pass-through left
without an identity check, so a rebuild there would have gone unnoticed while
the body and length stayed the same. It requires the origin's own object now,
like the other five.

The proof that an already-found landing really is rewritten checked the
canonical and og:url but not twitter:url, though the rewrite normalises all
three. A regression in that one replacement alone would have passed.

npm run check: exit 0, 190 tests. Verified by mutation: rebuilding the answer on
the not-owned branch, and breaking the twitter:url replacement alone, each turn
a test red.

* docs(invite): put the RFC deviation where contributors read, not only in the code

Three test-strength and documentation points from the confirmation round, which
again reported nothing that changes behaviour.

The Range case asserted the forwarded request but discarded the answer, so a
regression that stripped the header and still treated the response as partial
would have passed. It asserts the answer too.

The charset matrix collapsed every case to a status. It now checks what each
outcome means: a rewritten answer is promoted, declares UTF-8 and carries the
injected title; an untouched one keeps the origin's status, type and bytes.

The deliberate deviation from RFC 9110 §13.1 was stated in the code but not in
either document, though both describe the surrounding behaviour. Contributors
read those first, so both now say it and why.

npm run check: exit 0, 190 tests.

* docs(invite): separate the two kinds of precondition in the reasoning

The confirmation round accepted the deviation but caught the justification
being too broad. A tag or date condition would indeed need a validator minted
for the rewritten document. A wildcard one would not: `If-Match: *` and
`If-None-Match: *` ask only whether a current representation exists, which is
already known here. They are dropped for uniformity, not out of necessity, and
the comment now says which is which.

The behaviour is unchanged; only the stated reason was wrong for half of it.

npm run check: exit 0, 190 tests.

* test(invite): pin the two hand-off URLs and the path boundary

The last two mutation gaps from the confirmation round.

The Android and iOS alternate links were asserted by attribute name only, so
swapping the two would have passed while Android users were sent to the iOS
hand-off and back. Both hrefs are pinned now.

shouldRewriteItunesBanner matches its prefixes at a segment boundary, but no
test used a path that merely starts with the same letters, so dropping the
slash would have gone unnoticed and pulled /invitee and /promotion into the
rewrite. Four negative cases cover it.

npm run check: exit 0, 190 tests. Verified by mutation: swapping the two
hand-off URLs, and widening the prefix match, each turn a test red.

* fix(invite): recognise a landing shell by two marks, not one

The release round raised four points, and this closes all of them.

The one that touches behaviour: the shell was recognised by a single substring,
so an error document carrying `id="state-loading"` anywhere — a comment, a
script, a copied snippet — would have been promoted to 200 and dressed in an
invitation's metadata. The shipped 404 page does not carry it, and a contract
test pinned that, but nothing covered an error document the site does not ship,
such as an edge error or a challenge page. Recognition now needs both that id
and `aria-busy="true"`, which the two landings carry and nothing else arrives
at by accident. The case that used to be a documented boundary is now simply
handled: such a page keeps its 404 and its own body.

Three coverage gaps for behaviour that was already correct:

- `/promo` exactly was never tested, so deleting that clause would have stopped
  the promo landing being rewritten without a red test.
- The replace branch of the alternate-link upsert had never run: no fixture
  carried an existing link. Both attribute orders are covered, and the old href
  has to be gone rather than joined by a second tag.
- The iPad Twitter app card was emitted and asserted nowhere.

npm run check: exit 0, 191 tests. Verified by mutation: dropping the second
mark, removing the exact `/promo` clause, giving the iPad card a wrong URL, and
making the upsert always append each turn a test red.

* docs(invite): say two marks everywhere the code now needs two

The round on the previous commit found only its own after-effects, and this
clears them.

Both documents still described the guard as keying on one mark. That is the
drift this series keeps correcting, one commit after introducing it: they name
both now.

The same singular sat in the middleware comment, in the test file's header and
in two test titles.

Two claims of mine were wrong rather than merely stale:

- The commit that added the replace-branch test said it covered both attribute
  orders. Both fixtures put the attribute before the href, so the branch for
  the other order had still never run. A third fixture covers it, and disabling
  that branch now turns the test red.
- The Latin-1 test said its body carried the landing marker so that only the
  charset guard stood between it and the rewrite. With two marks required and
  only one present, that was no longer true — the marks check would have
  blocked it as well. The body carries both now, so the test isolates what it
  claims to.

npm run check: exit 0, 191 tests.

* docs(invite): the body is never stale; it is the status that diverges

"Never wrong, only larger than it had to be" claimed too much. The body a
client gets is always the current representation, so that half holds. The
status is the part that deviates: RFC 9110 puts 304 and 412 at SHOULD, and
this pass answers 200 instead. The comment now separates the two.

npm run check: exit 0, 191 tests.

* test(invite): the marks are attributes, not words

The last open point from the logic lane. Nothing in the suite carried both
words without the attribute syntax, so loosening the marks from
`id="state-loading"` / `aria-busy="true"` to bare `state-loading` /
`aria-busy` would have passed — and any page mentioning both in prose would
then have been promoted to 200 and dressed as an invitation.

A page that does exactly that is now a case, and it keeps its 404 and its body.

npm run check: exit 0, 191 tests. Verified by mutation: loosening the marks to
bare words turns it red.

* docs(invite): a comment that its own next line disproves

The last point from the round on 880311e. The comment still called the
attribute-in-a-comment case a boundary the check cannot see, while the
assertion three lines below asserts a 404 because the second mark closes it. It
says what is true now, and what it used to be.

npm run check: exit 0, 191 tests.

* docs(invite): say what the marker check does, and pin what makes it enough

The marker check is a substring test. Four comments and two doc paragraphs
claimed more than that, or more than the code around them holds:

- two comments called a pass-through "passed on as they are" and "handed on
  exactly as it came", which is true of a GET and not of a HEAD, whose answer
  is rebuilt without a body three lines below;
- the landingStatus JSDoc used "rewrite" for both the tag injection that has
  already happened and the status promotion it performs itself;
- the isLandingShell JSDoc said two marks together are "not something another
  page arrives at by accident", which the check does not establish: it never
  asks whether the marks share an element;
- CONTRIBUTING and README said these paths emit no validator, which holds for a
  rewritten landing and not for a pass-through, where the origin's ETag is kept
  and a test already pins it.

What actually makes two substrings enough is a property of the shipped site,
so it is now asserted instead of described: of every HTML page under public/,
only the two the Function is routed to read as a landing shell. The
account-merge and Aktionariat shells carry the id and no aria-busy, the 404
page carries neither. The test goes red the day one of them gains the second
mark, which is the day the guard would have to become a real parse.

* docs(invite): correct three claims, and pin the inventory the new test rests on

Three statements from the previous commit did not hold, and one measurement
was missing:

- CONTRIBUTING said the marks are carried by the landings and by "no page
  served under these paths", which contradicts itself: the landings are the
  pages served under these paths. It now says no other page the site ships.
- The isLandingShell JSDoc credited the function with establishing that no
  other shipped page reaches both marks. The function performs a substring
  test and establishes nothing about other files; the property is held by the
  test, which the docstring now says.
- The path gate excludes twelve asset-like suffixes, and a review read that as
  leaving /invite/AB.JSON without the promotion. Measured on the deploy
  instead of argued: such a path is answered with the site's own 404 page, not
  with the landing shell, so the promotion would refuse it on the marks in any
  case. What a code like that loses is the landing itself, which is the
  follow-up #30 left open. The gate now says so.

The repo-wide test pinned only three of the six shipped pages by name, so a
walker that quietly stopped finding the others would still have passed: the
filtered list would simply have had nothing left to disagree with. It now pins
the whole inventory first, and the doubled-slash and /invites path boundaries
are asserted rather than reasoned about.

---------

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants