docs(og): gate the OG card URL's dotted final segment, and name the coupling on both sides - #13702
Merged
os-project-manager merged 1 commit intoAug 31, 2026
Conversation
…upling on both sides `getPageImage()` in `apps/docs/lib/source.ts` appends an `image.png` marker to every Open Graph card URL. That marker is load-bearing twice, and only the first is visible from the code: the OG route slices it off to resolve the page, and its DOT is what keeps the URL out of `proxy.ts`'s locale rewriter (the matcher excludes any path containing a dot). Measured: `/og/docs/ai/agents` is rewritten to `/en/og/docs/ai/agents`, which is not a route; `/og/docs/ai/agents/x.png` is not rewritten. The marker's name is free; its dot is not. Both halves: - Cross-referencing comments on each side, each naming the other file and what breaks. - `check-docs-locale-catch-all` gains a limb asserting that the URL `getPageImage()` builds ends in a final segment containing a dot. It reads the marker AND the template that turns the array into the URL, so the assertion cannot survive as a check on a variable that no longer reaches the URL. The matcher-excludes-dots half is the condition the script already reads; it is reported in the summary, not re-asserted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
This was referenced Aug 31, 2026
os-project-manager
marked this pull request as ready for review
August 31, 2026 08:20
os-project-manager
deleted the
claude/issue-12326-og-marker-dot-coupling
branch
August 31, 2026 08:41
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.
Fixes #12326
The coupling, re-verified rather than inherited
getPageImage()inapps/docs/lib/source.tsbuilds every Open Graph card URL as/og/docs/+ the page slugs + animage.pngmarker. That marker is load-bearing twice,and only the first is discoverable from the code:
app/og/docs/[...slug]/route.tsxresolves the page withsource.getPage(slug.slice(0, -1))— the marker is the sacrificial segment that slice discards. Its name is irrelevant to that.
apps/docs/proxy.ts's matcherexcludes any path containing a dot, so a dotted final segment skips the proxy entirely. A
dotless marker is rewritten under the locale prefix, which is not a route at all — the
app/og/tree is top-level, not underapp/[lang]/.Both premises re-measured on this branch's merge base, not carried over from the card:
proxy.ts's matcher is still, byte for byte,'/((?!api|_next/static|_next/image|favicon.ico|.*\\..*).*)'.app/og/is a top-level route directory; the only locale-prefixed tree isapp/[lang]/, whosechildren are
docs/andblog/— so the rewritten path really has no route.That is the whole invariant, and until this PR it lived only in an issue comment.
Both halves ship
1. A cross-referencing comment on each side.
apps/docs/lib/source.tsnow namesapps/docs/proxy.ts, the OG route, and what breaks;apps/docs/proxy.tsnow namesgetPageImage()andlib/i18n.tsas the two surfaces itsdot-exclusion limb holds up, in opposite directions. Both point at
pnpm check:docs-locale-catch-all.2. The mechanical half —
check-docs-locale-catch-allgains a limb.The charter's "extend or companion" choice was resolved as extend, for three measured reasons:
proxy.ts's matcher. A companion would either duplicatethat parse or import from this script — a dependency in a family that is deliberately
dependency-free so it can run with bare
nodeand no workspace install.dependent; the OG marker is the other. One file now answers the whole question.
lint.ymlstep, a new entry in thecheck-self-test-wiredpopulation and asecond self-test output surface, for an assertion of roughly twenty lines.
What the limb asserts is exactly the half the charter named and nothing more: the URL
getPageImage()builds ends in a final segment containing a dot. It deliberately does notre-assert that the matcher excludes dotted paths.
It reads two things, not one. The marker literal alone would be a check on a variable that may no
longer reach the URL, so the limb also requires the returned template to still end with the joined
segments — otherwise the last array element is not the URL's final segment and the dot assertion
would be pinning the wrong string. Every shape it cannot parse (builder renamed away, array gone,
marker not a literal, url no longer built from a template) is a finding, never a quiet pass.
The ablation — the assertion observed going RED
Run against the real tree, on top of the implementation commit, with the mutation confirmed on disk
before any reading was taken (no build leg: the gate reads source text, it does not import built
artifacts).
Control ablation — the same edit keeping the dot (
'image.png'to'card.jpeg'), mutationlikewise confirmed on disk:
VERDICT-EXIT prod=0, summaryOG card marker "card.jpeg" ends in a dot: true. So the limb pins the dot, not the filename,which is what the measurement says matters.
The same two directions are pinned in
--self-testas well, over the realcheckApppath: a markerstripped of its dot, a marker dropped entirely, a url that stopped ending in its segments, a missing
builder — each observed failing — plus a renamed-but-dotted marker observed staying green.
Self-test assertion count went 12 to 21.
Was a running server needed? No.
Both facts are static: the matcher is a string literal in
proxy.tsand the marker is a string literalin
lib/source.ts. The gate compiles the one and applies it to the other, with no server, no networkand no workspace install — the same dependency-free shape the rest of the file already had.
Verification
Derived from the actual diff with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(36 families, 39 commands), run at
6bcc114bc— every reading below taken with the exit code capturedbefore any pipe.
pnpm check:docs-locale-catch-all(productionexit=0;1 top-level dynamic segment(s), 1 guarded; dotted paths bypass proxy.ts: true; OG card marker "image.png" ends in a dot: true (that URL skips proxy.ts: true)),check:nul-bytes,check-self-test-wired,check-self-test-workflow-commands,check:workflow-status-functionsand the other four workflow parsers that read the edited
lint.yml.node scripts/check-test-completeness.mjs-> exit 3 = NOT MEASURED by the script's own words(it needs a saved
turbo run testlog). Not a finding.pnpm check:type-check-debt-> its--re-measureleg refuses to run without the built packageclosure, and says so: measuring from here would measure a different world. Declared narrowing,
with its evidence: the sibling
pnpm check:type-check-coverage(same script, same--self-test,structural half) is green, and this diff contains no input of that gate — no
packages/**, nopackage.json, notsconfig*.json, no*typecheck-debt.json. CI builds the closure and runs it.eslint .narrowed to the changed files, and the narrowing measured rather thanassumed: population decided by eslint's own config resolution (it reports
.github/workflows/lint.ymlas
File ignored because no matching configuration was supplied, so the YAML is outside itspopulation by eslint's own answer); 3 files linted, counted from
--format jsonoutput length;0 errors, 0 warnings. Invariance for untouched files: this repo runs one
eslint.config.mjswhichnever enables type-aware linting for any file (no
parserOptions.project, no typed rules — statedand positively controlled in that config's own header), so nothing in this diff can move an untouched
file's verdict.
apps/docstypecheck (next typegen && tsc --noEmit) narrowed away, mechanically: the entireapps/docsdiff is added comment lines and nothing else — every added line matches a commentprefix and there are zero removed lines. The one thing that does read those comments as data is
check-docs-locale-catch-allitself, which parses both files and is green.A measurement the reviewer should see
The dispatch charter states that the complementary half — that
proxy.ts's matcher excludes dottedpaths — is already asserted, and forbids duplicating it. Measured against the script: it is
reported, not asserted.
stats.dottedBypassesProxyis a condition; when it is false the scriptdoes not fail, it relaxes the catch-all requirement and stays green (self-test case 6 pins exactly
that behaviour, deliberately).
Consequence, stated plainly rather than acted on: after this PR, a change to the
lib/source.tssideof the coupling is gated, and a widening of the
proxy.tsside still goes green on both limbs eventhough it 404s the same surface. The charter's boundary was respected — nothing here re-asserts the
matcher — and the fact is surfaced instead, in three places: this section, the new header section in
the script, and the gate's own summary line, which now prints
(that URL skips proxy.ts: true)forthe built OG URL so the value is visible in every CI log. Closing that half is a one-line change the
seat can call.
Changeset
None, and
skip-changesetapplied: this PR releases nothing. The twoapps/docsedits arecomment-only in a private, unpublished app; the remaining files are a CI gate script and a comment
plus a step name in
lint.yml— the caselint.ymlitself names as the textbook one for the label.Generated by Claude Code