Repository navigation
CourseForge 4.8.0
The punctuation pass reads a quotation mark the way a reader does
4.6 taught CourseForge to set a generated page's punctuation the way its language sets it. It read each mark by position — a mark after a space opens, a mark after a word closes — and position turned out not to be enough, twice.
**"Wort"** came out as **"Wort“**. A Markdown asterisk stood where a space should have been, the opening rule declined the mark on its account, and the closing rule took it — so a bold quotation ended with two closing marks, and the first one was still straight. And correct French came out wrong: in « bonjour » et, the closing guillemet has a space on both sides, exactly like an opening one, so every » was turned back into a « — on text that had been right before the pass ran.
A mark is now decided by three signals, in order.
- Position first, with Markdown emphasis stepped over on the way to it, so a quotation with asterisks around it is still a quotation.
- Nesting depth second, which is what tells a closing guillemet from an opening one when both of its neighbours are spaces and position has nothing left to say.
- The glyph last, for a mark that only ever points one way.
And what gets written is not what was read: a closing mark takes the pair its own opening mark took. That is why „Wort" — the thing the models actually write — comes out as a pair at all, and why a double quotation inside a double quotation comes out as „… ‚…‘ …“ whichever character the model typed. The depth is forgotten at every blank line, heading, list item and table row, so one unbalanced mark costs its own paragraph instead of every paragraph after it.
The rest of the pass grew with it. None of this is AI: it is rules, it runs in milliseconds, and it costs nothing.
- An em dash between two words becomes a spaced en dash in the languages that set one that way — the single most reliable thing a model writing German reproduces from English.
1990-2000becomes a span, while2026-09-01,T-34and1-2-3are left alone.5"after a digit with no quotation open is a measurement and becomes5″, while the"closing"42"is still a quotation mark, because the depth tells them apart.don't,'90s,rock 'n' rollanddogs' bowlseach get the apostrophe they want, and none of them is mistaken for a quotation.- Four spaces become one, a space before a comma goes,
( innen )closes on its content, three blank lines become one, and trailing whitespace goes — except the two spaces that are a line break in Markdown, and except inside a table row, whose padding is what makes its source readable. - HTML comments and characters escaped on purpose join fenced blocks, inline spans, links, addresses, tags, formulas and table alignment rows as things that are lifted out and put back untouched.
- Polish, Czech, Slovak, Hungarian, Russian, Ukrainian, Greek, Swedish and Finnish now get their own marks rather than English ones.
…and it can be pointed at a course that is already written
All of that runs when a page is stored, which was no help at all to the pages stored before it existed, stored with the setting switched off, or stored by 4.6 and 4.7 with the bugs above in them.
Course → Settings → Punctuation now runs the same rules over a course that is already finished. One chapter and its pages have their own button beside Publish chapter; one page has one in the inspector.
It asks first. The button runs the entire pass with the writing switched off and says what would change — "of 3 pages and 2 chapters, this would change 1 chapter and the course description", with the first forty of them named — and only then offers to go through with it. The number in the dialog is the number that changes, because it comes from the same run.
It runs whatever the profile says, and that is the point rather than an oversight. The per-profile switch answers "correct the pages I am about to generate"; this button is somebody standing in front of a course they already have. A setting that turned the second question into a no would be a setting that refuses an instruction.
It corrects every page's Markdown and title, every chapter's title and description, and the book title and description, and rewrites the outline from the titles afterwards — the same repair renaming a page has always made, and necessary because applying a structure matches by title. A page's extra context and the course's research notes are deliberately left out: they are instructions to the writer, not text anybody reads.
A page that needed nothing is not written at all — its timestamp does not move and it does not go newly out of sync with a wiki over a change that never happened. A page that did change does differ from what BookStack holds, and says so until the next publish.
Over MCP
fix_typography, with course_id, an optional chapter_id or page_id (a page wins over a chapter, which wins over the whole course), an optional language to set the text as something other than the course's own, and preview to be told what would change without changing it. It is flagged destructive and idempotent, which is exactly what it is: it rewrites a whole course in place, and doing it twice is doing it once.
Upgrading
Nothing to do, and no schema change. Existing pages are left exactly as they are until somebody asks for them to be corrected. Profiles that correct new pages go on doing so, with the better rules.
344 tests pass, 21 of them new. Verified in a browser against a scratch database seeded with the wrong kind of German: the page button, the chapter button and the course dialog, the preview naming what it would change and the second run saying there was nothing left, code fences and links and table padding surviving all of it, and the outline coming back rewritten from the corrected titles.