Close CJK-punctuation-flanked bold in ja / ko / zh-Hans, and gate it in the built HTML - #201
Conversation
A `**bold**` span whose delimiter sits against CJK punctuation on the
inside and a CJK letter on the outside is not a flanking delimiter run,
so CommonMark emits the asterisks verbatim. Readers of the four CJK
locales saw literal `**` mid-sentence on 24 built pages.
Repaired by moving the delimiter, never by dropping the emphasis, in
three shapes:
* closing delimiter blocked by sentence punctuation inside the span
-> move it inside the punctuation: `...ない。**モ` -> `...ない**。モ`
* closing delimiter blocked by a trailing parenthetical gloss
-> move it left, past the gloss: `**用語(gloss)**は` -> `**用語**(gloss)は`
(this also matches the English originals, e.g. `A **detective** control`)
* the span is a quoted phrase and its quote blocks a delimiter
-> move both delimiters inside the quotes: `**"..."**` -> `"**...**"`
Where the parenthesis is mid-phrase rather than trailing, the delimiter
moves right instead so the emphasized phrase stays a complete unit.
zh-Hant is generated: only the zh-Hans sources were edited and
`pnpm gen:zh-hant` (run by `pnpm build`) carried the six twins.
Measured at 4dd647c: 24 built pages carried a literal `**` outside
`<code>`/`<pre>` (ja 6, ko 6, zh-Hans 6, zh-Hant 6), from 37 inert
delimiter runs across 18 authored files. After this change: 0.
Part of #174
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
Nothing in the chain saw the defect the previous commit repaired: the
markdown is valid, so this script's MDX pass, `astro check`, `astro
build` and `seo:smoke` all pass on a post that renders `**` to the
reader. Without a gate the next translated post reintroduces it silently,
and only a reader of the CJK locales would ever notice.
`content-lint.mjs --dist` reads the BUILT HTML and fails when a blog page
carries a literal `**` outside `<code>`/`<pre>`. Two properties it is
built on:
* it asserts over `dist/`, after `astro build`, because the defect
exists only in rendered output — an MDX-level check would have to
re-implement CommonMark's flanking rule tolerantly, and a tolerant
re-parse of the thing being gated is how a gate stops gating;
* the `<code>`/`<pre>` exclusion is structural, not a regex that hopes.
The emitted HTML is parsed and those elements skipped. Real pages
depend on it: `<code>138****5678</code>` is a masked phone number and
an English post quotes `**` in a snippet — both correct, both green.
A run that measured nothing fails rather than passes: no `dist/`, or a
`dist/` with no blog pages, reports the reason and exits non-zero, so the
gate cannot go quietly green if the build layout moves.
Wired into `pnpm build` after `astro build`, which is why package.json
moves here too — CI already runs `pnpm build`, so no workflow change.
Part of #174
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
|
ACCEPT — with the seat's own ablation of the guard, which is the only thing that proves a guard. The population is gone from the built output. My raw grep over built blog HTML at the union: 3 literal That first one is a better argument for ruling 3 than the ruling was: it is a glob pattern in a code sample. A regex guard would have gone red on Ablation, run by the seat rather than read off the report. Injected one CJK-punctuation-flanked bold span into
And the failure message is the best thing in this PR. It does not just say what is wrong — it states the CommonMark rule that caused it, explains why CJK prose trips it where English does not, and shows the repair: An author who hits this at 2am will fix it in thirty seconds and understand it afterwards. That is the difference between a gate that gets obeyed and a gate that gets worked around. Union with current On the Three more things worth naming:
Landing on Generated by Claude Code |
Fixes #174
A
**bold**span whose delimiter touches CJK punctuation on the inside and a CJK letter on the outside is not a flanking delimiter run, so CommonMark emits the asterisks verbatim. Readers of the four CJK locales saw a literal**mid-sentence. English almost never trips it because English puts a space where CJK puts a letter.Two commits: the repairs, then the guard that keeps them repaired.
1. Population, re-measured at my own head (ruling 7)
Built from
origin/main@4dd647cand counted structurally — parsing the emitted HTML and skipping<code>/<pre>rather than grepping:**in prose24 pages, from 37 inert delimiter runs across 18 authored files. The card's 24 at
3b6d563was a rawgrepcount; mine is the structural one, and the composition differs (the card had ja 5 / ko 5 / zh-Hans 7 / zh-Hant 7). The two numbers agreeing is a coincidence — a raw grep at4dd647creturns 27, three of which are correct and must stay green:en/blog/how-to-point-your-agent-at-objectstack/—**quoted inside a code samplezh-Hans+zh-Hantobjectos-agent-permission-boundaries/—<code>138****5678</code>, a masked phone numberThose three are exactly why ruling 3 asks for a structural exclusion instead of a regex.
I cross-checked the source-level analysis against the built HTML hit-for-hit: all 24 pages agree on their hit counts, and neither side lists a page the other does not. That is what makes the worklist exact rather than a best effort.
2. The repairs — the delimiter moves, the emphasis never goes (ruling 4)
Three shapes, applied consistently. In every case the emphasized phrase stays a complete unit:
…しか呼べない。**モto…しか呼べない**。モ**用語(gloss)**はto**用語**(gloss)は**"引用"**のto"**引用**"のWhere the parenthesis sits mid-phrase rather than trailing, the delimiter moves right instead — the card's own canonical repair (
**开箱即用联邦(turn-key federation)体验**) is that case. Same principle, opposite direction: keep the emphasized phrase whole.The "move left past a trailing gloss" choice also lands where the English originals already put the bold, which is a useful independent check on it:
A **detective** control tells you…/A **preventive** control stops…the **confused deputy**/**least privilege.**No span was dropped, and no sentence was reworded.
3. zh-Hant came free, and I proved it rather than assuming (ruling 5)
Only
index.{ja,ko,zh-Hans}.mdxwere edited — 18 files. The 6index.zh-Hant.mdxtwins in the diff are output ofpnpm gen:zh-hant, whichpnpm buildruns first. Their diffs mirror the zh-Hans edits with Traditional characters intact.git status --porcelainis empty afterpnpm buildat the final commit, so regeneration is idempotent and nothing was hand-edited.4. The guard (rulings 1, 2, 3)
scripts/content-lint.mjs --distfails when a blog page renders a literal**outside<code>/<pre>.<code>/<pre>skipped by element, not by a regex hoping to spot a fence.<script>/<style>are skipped too — their bodies are code, where**is ordinary and no reader sees it.dist/, or adist/with no blog pages, prints the reason and exits non-zero. Verified: from a directory with nodist/it exits 1 withcontent lint --dist found no built blog pages under dist/. Without that, the gate would go quietly green the day the build layout moves.*are resolved before matching, so the rule cannot be satisfied by an escape that changes nothing a reader sees.package.jsonmoves here because the guard has to run afterastro build—pnpm buildnow ends with it. CI already runspnpm build, so no workflow change was needed.5. Ablation — the gate shown failing (ruling 6)
One CJK-punctuation-flanked bold span injected into
content/blog/business-app-in-16k-tokens/index.ja.mdx, both legs rebuilt, mutation confirmed on disk before each measurement, restore proven by blob hash.Mutation leg — anchors on disk went good 1 to 0, bad 0 to 1; blob
4da5e6dto3c6d5f5; rebuilt; gate red:Restore leg — blob back to
4da5e6d, matching the HEAD blob (a non-empty hash, compared explicitly; an empty one is treated as failure, not as "nothing to compare");git diff HEADempty; rebuilt again so no mutated render survives indist/to poison later readings; gate green:The restore script uses absolute paths in its
trapand re-derives the repo root, so a mid-run kill cannot leave the tree mutated or resolve the checkout against the wrong directory.6. Browser pass, one page per affected locale (ruling 8)
Served
dist/and drove Chromium over one repaired page in each locale. The check is not "no asterisks": it requires a real<strong>carrying the repaired text and a computedfont-weightthat is actually bold, because an element that exists but renders at weight 400 would satisfy a DOM check while showing the reader nothing.<strong>**in visible textbusiness-app-in-16k-tokensコンテキストサイズのソフトウェアbusiness-app-in-16k-tokens컨텍스트 크기의 소프트웨어vibe-coding-technical-debt-2026理解债vibe-coding-technical-debt-2026理解債4/4 pass. Screenshots of the repaired sentences rendering bold are described in the report comment on #174.
Gates
All at
f41147f, tree clean, exit codes captured before any pipe:pnpm content:lint✓ content lint passed (335 files, 44 glossary terms checked)pnpm content:lint --published✓ content lint passed (335 files, 44 glossary terms checked)pnpm check- 0 errorspnpm build867 page(s) built in 60.63s+✓ content lint --dist passed (474 built blog pages checked)pnpm seo:smokeSEO smoke test passed (866 HTML pages checked)One note for the reviewer
The gate is scoped to blog pages, as ruling 3 states. I also ran the same structural scan across every built page and it is currently at zero, so widening the scope site-wide would be free today. I did not widen it on my own: glossary and marketing pages are authored in
.tsrather than markdown, so whether a literal**there is a defect or intended text is a call for the seat, not for me.Generated by Claude Code