Let a CV emphasise a word, and give the editors somewhere to do it - #102
Merged
Conversation
Adds the parser for `**bold**`, `*italic*` and `***bold italic***`, and the plain-text form everything that is not a renderer compares against. Nothing calls it yet. This is the successor to the `CvBullet.label` field removed in #98: that commit deleted the structural bold lead-in on the grounds that the technique which actually works is emphasis *within* the sentence, which a separate field cannot express. Until now neither existed. Two rules are worth the attention they cost: Asterisks only, never underscores, because `snake_case` and `__init__` are ordinary content in a technical CV and `_italic_` would mangle them. No intraword emphasis — a delimiter with a letter or digit on both sides neither opens nor closes. CommonMark permits this for `*`; permitting it here would italicise `A*A*A`, which is a UK A-level grade and a realistic `Education.grade` value. The same rule removes this feature's one genuine ATS hazard, since emphasis inside a word splits it across two text-showing operators that some extractors rejoin with a space. An invariant test pins the property everything downstream leans on: `stripCvMarkup` is defined as the concatenation of the parsed runs, so the text the PDF draws and the text search compares can never disagree. It also pins that no non-empty string strips to empty — unmatched delimiters are literal, and two delimiter runs can never be adjacent — so the blank checks in `CvVaultPruning` stay correct on raw strings and need no markup awareness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsmGo6QAsQ7RQpNxKUSWeU
Routes the text a user types through the markup parser on its way to the page, so `**bold**` reaches the reader as bold rather than as asterisks. The four Roboto faces were already loaded and cached; nothing new is embedded that was not already sitting in `CvFontSet` unused. Covered here: bullets, the summary and references prose, hobbies, skill categories and labels, language names, the headline, and the work-authorisation sentence. Emphasis is laid *over* a token rather than replacing it, by passing null for an absent flag — so markup can add bold or italic but never take it away, and a template's type scale still wins. Fields glued together before rendering are parsed one at a time and joined with unparsed separator spans. Joining first would let an unclosed marker in one hobby pair with one in the next, which is emphasis leaking across a boundary the user cannot see. The work-authorisation line is the one part of the contact block that is parsed. It is a sentence someone wrote; the rest are addresses an ATS matches with regexes over the text layer, where a stray asterisk is how a match stops happening. URLs stay literal for a different reason: a link is also its own destination, and markup in the printed text but not the destination is a silent divergence. `bodyText` becomes concrete and the three templates keep only what they actually differed in — `classic_centered` overrides `bodyAlign` to justify, and the other two overrides were identical to the base and are gone. Text with no asterisk takes a fast path that rebuilds the exact `pw.Text` it did before, so a CV carrying no emphasis is byte-identical. That is what lets all seven golden baselines stay valid, and the suite confirms it. The new PDF tests assert the observable fact rather than the intent: the italic face is absent from a plain bullet and present in an italicised one, and the bold-italic face appears only for `***`. Also corrects a claim that had gone stale in `cv_design_tokens_pdf.dart` and CLAUDE.md — it is not the only file there importing `pdf`, and has not been for some time. The live convention is the `*_pdf.dart` suffix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsmGo6QAsQ7RQpNxKUSWeU
A role, a company, an entry location, a project or publication title, a citation, a qualification, a grade, education details — every one of these is a field someone edits, several of them newly so. Typing `**bold**` into a job title and getting asterisks back would read as a bug rather than as a boundary, so the header methods each template overrides now go through the parser too. Two shared row helpers took strings and now take spans, because only the caller knows which side is user text: `compact`'s `_labelledRow` and `classic_centered`'s `_headerRow`. The right-hand side of the latter carries an entry location in some calls and a composer-formatted date range in others, and those must not be treated alike — `_metaSpans` marks the ones that are never parsed. Every header that glues several fields onto one line now parses them individually and joins with unparsed separator spans. `compact` and `classic_centered` folded grade and details onto the qualification line with a comma, and `photo_header` folded institution, location, grade and details after an " at "; joining first would have let a marker in one field pair with a marker in the next, which is emphasis crossing a boundary the user has no way to see. The new test asserts this per template rather than once, since the methods are overridden separately in each. Bold-italic is the discriminator: no template sets both on a token of its own, so the fourth face can only be embedded because `***` asked for it. All seven golden baselines still match, and the full suite is green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsmGo6QAsQ7RQpNxKUSWeU
The PDF renders emphasis; until now the app around it still displayed the raw asterisks, so a bolded role read as `**Senior** Engineer` on its own Vault card. This adds the Flutter half of the adapter and points the display surfaces at it: the Vault summary cards, Studio's collapsed field rows, and the entry and bullet titles in its selection lists. Editing surfaces are deliberately left alone. A field being edited shows the characters someone typed, markers included, because those characters are what they are editing — and hiding them would leave no way to remove one. The Flutter spans carry only `fontWeight`/`fontStyle`, never a whole `TextStyle`. Flutter merges nested span styles down the tree, so the enclosing `Text.rich` keeps supplying the colour, the italic an empty field is drawn in, and the strikethrough on an omitted one — all three of which a fully-styled span would have silently dropped from `StudioEntryFieldRow`'s omitted rows. Search now compares what a field prints rather than what it stores. Without this, bolding a word removes it from search: "deployment" would no longer find `**deployment**`. That is three call sites rather than one, since two of the Vault's filters do not go through the shared helper. The skill link picker's exact-match check needed it too, or typing "Dart" beside a skill named `**Dart**` offers to create a duplicate. The LLM length guard now measures rendered characters. Its own comment says the bound is sized against the page, and a `**` occupies none of it — counting raw characters would make the limit tighten as someone added emphasis. A separate, looser raw bound keeps a field of pure asterisks away from the parser. Both prompts were silent on formatting, which meant a tailoring or translation pass would preserve, strip or invent markers at random. They now say to carry emphasis across with the phrase it marks, never to add any, and that a lone `*` is an A-level grade rather than half a pair. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsmGo6QAsQ7RQpNxKUSWeU
A field that stores `**bold**` and draws it as literal asterisks tells you nothing about what will print. `CvMarkupEditingController` paints the runs live — the word bold, the markers dimmed — while leaving every character and every offset exactly where it was, so the caret, selection and backspace behave as they do in any other field. Dimmed rather than hidden, deliberately. Markers that vanish are markers you cannot delete, and a field showing something other than what it stores is one you cannot reason about while editing it. Bold and italic buttons appear above a field while it has focus, with Cmd/Ctrl+B and I bound to the same lever. Formatting is undiscoverable without a control saying it exists, but a button row over every field in a dense Vault panel is noise; at most one field is focused, so at most one row is ever on screen, and the space is reserved at rest so revealing it cannot shove the field out from under the cursor that just clicked in. The toggle is modelled as emphasis bits rather than as wrapping and unwrapping a string, which a test caught: applying italic to an already bold word was stripping a marker instead of composing. A delimiter run's length is which emphases are on, so applying one flips its bit. Bold then italic gives bold-italic, removing bold from a bold-italic word leaves it italic, and pressing twice returns the original text rather than accumulating `****`. Which fields offer this is opt-in and matched exactly to what the renderer honours, so the fields showing a bold button are the fields that would draw one. Contact addresses, URLs and — via a flag on Studio's editor — the AI job-description box get nothing, that last one because a pasted job ad is never printed and emphasis in it would mean nothing. Also fixes a real parser bug found while building the controller: a delimiter stranded inside a pair that closed over it was being dropped from the text rather than only from the matcher, so `*a*a*` printed "aa" and silently lost an asterisk someone had typed. CommonMark removes those from the stack, not from the page. The invariant test that should have caught it only checked the output had not grown; it now checks that every character which is not a marker survives verbatim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsmGo6QAsQ7RQpNxKUSWeU
Studio's undo button reported that a field had been rewritten for this CV but never what it had been rewritten *from*, so the only way to read the Vault's own wording was to revert and lose the rewrite. An open editor now shows it above the box, dimmed, and only when it is worth showing: not for an untailored field, not when the Vault is empty, and not when the override happens to match — printing the same sentence twice is noise rather than context. It is drawn through the markup renderer, so the original reads the way the CV prints it rather than as raw asterisks. `TailorableField` carries the Vault value to make this possible. It was already in scope at all fifteen construction sites; none needed new plumbing to reach it. Two duplications go with it. The row already names the field directly above its own editor, so the editor no longer sets a label of its own — that was rendering "Grade" stacked over "Grade". And the collapsed preview kept showing the value while the box below held the same text, which made one field read as two; the value now gives way while editing and the label stays. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsmGo6QAsQ7RQpNxKUSWeU
The product's premise is a document that fits, and until now the editor said nothing about length — a 700-word summary was discovered in Studio's preview, or by an LLM pass silently refusing to rewrite it. Prose fields now show how much room is left, but only once that is nearly spent. A permanent counter under every field would be noise for a bound almost nothing reaches, and the bound exists to catch a field that has swallowed the document rather than to police how much anyone writes. Past the limit it states the consequence instead of a number, because the number is not the actionable part. It counts printed characters, so adding emphasis never brings the warning closer — the markers occupy no page. That is the property the test pins: a passage comfortably inside the limit stays silent even when every word in it is bolded and the raw string is well over. Single-line fields get no budget. A role or a skill label could not plausibly reach a page-sized limit, so a counter there would be chrome for something that never happens. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsmGo6QAsQ7RQpNxKUSWeU
Two things that were making the editors harder to use than the writing deserves. Studio's editor pane is about half a window wide with the preview beside it, and a nested field row keeps roughly 300px of that once the nav column, indents and icon cluster are taken out. That is a slot to write a sentence through, and the summary and the longer bullets are paragraphs. Prose fields now offer a roomier dialog, built the way every other dialog here is — through `DialogService`, registered by the CLI — rather than an in-widget `showDialog` that would have been the first of its kind. It commits on Save rather than per keystroke, so backing out of the detour leaves the field as it was. The About you panel was nine identical outlined boxes in one column: a name, an email and a four-line summary given the same weight and the same gap, with nothing saying they are three different kinds of thing. It now groups under the same headings the Vault's card list uses — who you are, how to reach you, what you say — so the panel reads with the rhythm of the list it was opened from rather than as a form to get through. Education keeps one subject and so gets no headings, which over a single field would read as a section that had lost its contents. Its prose field gets the wider gap instead, which is the whole signal needed to say that everything above it is a fact with one right answer and this is not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsmGo6QAsQ7RQpNxKUSWeU
Opening a second inline editor stole the caret from the first while both stayed open and both stayed tinted, with nothing saying which one was being typed into. Only one row holds an open editor now, which is how `StudioSkillSelector` already worked — the selector list was the odd one out for holding a set. An open editor's id also outlived the row that owned it. Dropping an entry from the CV hides its rows but left the id behind, so re-including that entry silently reopened an editor nobody had asked for. A page-level row — the headline, the summary, the references note — drew its text with a bare `TextStyle()`, inheriting whatever ambient default applied. It was the one type treatment in the pane that was not a deliberate choice; it now takes a token like everything else. And clearing a tailored field does not blank it: the override is removed, so the Vault's wording comes back. That is the right behaviour and it stays, but watching text reappear on its own reads as the edit having failed, so the box now says so before it happens — and only where the Vault actually has something to fall back to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsmGo6QAsQ7RQpNxKUSWeU
The parser next door is hand-rolled, which means this codebase owns the delimiter-stack algorithm and the class of bug that comes with it. It has already shipped one: a delimiter stranded inside a pair that closed over it was dropped from the text rather than only from the matcher, so `*a*a*` printed "aa". `cv_markup_test.dart` pins what specific strings mean and stays the readable specification. This file asks a different question — whether the parser can be made to misbehave — and needs a different oracle. On twenty thousand inputs nobody can say what the right runs are, but everybody can say the text must survive. The corpus is CommonMark's own emphasis and backslash-escape examples, which exist because they broke somebody's parser once. Their expected HTML is deliberately not asserted: this parser diverges on purpose, so the spec's answers are the wrong oracle while its inputs are still the best adversarial corpus going. Alongside them sit the things real CVs contain — A-level grades, UNC paths, SQL wildcards, shell globs, C pointers, combining accents, RTL text — and a seeded fuzzer for what neither list thinks of. The invariants are chosen to be provable rather than plausible. The sharpest is marker accounting: a matched pair always takes the same count from each end, so the number of asterisks consumed can only ever be even, and an odd count means one went missing. That check exists because the first draft of this file did not have it and did not catch the original bug — stripping asterisks from both sides before comparing is blind to a marker being dropped. Checked against a deliberately broken parser four ways. Dropped markers, broken coalescing and unspliced leftovers all fail here. Removing the intraword rule — which would silently italicise every A-level grade in the app — leaves every invariant intact and fails `cv_markup_test.dart` instead, which is the division of labour working rather than a gap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsmGo6QAsQ7RQpNxKUSWeU
#101 landed on the same field this branch had just taught to carry emphasis, and moved it. It is no longer one cell of the header's wrapped contact run — it is a sentence someone wrote, and as a single cell it could not break, so a long one ran off the right margin. It now prints on its own full-width line. That structure wins; the emphasis follows it there rather than staying where it used to be. Two conflicts resolved that way: the renderer keeps main's own-line layout with markup applied to its new home, and the Vault search keeps main's newly searchable field while still comparing what a field prints rather than the markers around it. The merge also needed a fix git could not have flagged. `photo_header` builds its own header, so #101 gave it a second, separate work-authorisation site — and that one auto-merged in clean as a plain `pw.Text`, silently outside this branch's coverage. Emphasis typed there would have printed as asterisks in one template out of three, with nothing failing. So the line now has a test, per template, asserting the bold-italic face is embedded only when the markup asks for it. Reverting the photo_header site to what auto-merge left behind fails it by name, which is the point: this field has moved twice in two PRs and should stop being able to move quietly. The `vault_view_cv_defaults_open` baseline is #101's, arriving through the merge byte-identical — not a local regeneration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsmGo6QAsQ7RQpNxKUSWeU
IanHeinrich
pushed a commit
that referenced
this pull request
Aug 26, 2026
Two files were fetched from Google on every page load: CanvasKit from gstatic.com and Roboto from fonts.gstatic.com. Both are Flutter Web defaults rather than choices anyone made here, and both sit badly in an app whose claim is that a CV never leaves the device — index.html already sets dartPdfJsBaseUrl locally for exactly that reason, and these two were simply missed. CanvasKit costs nothing to bring in-house: `flutter build web` already copies it into build/web, so those bytes were being deployed to Pages and then ignored in favour of a CDN round trip. The loader now points at them. Roboto is registered as a `fonts:` family from the four faces already bundled for the PDF, so CanvasKit finds it in the asset manifest instead of downloading it. The app now boots and renders with every non-origin request blocked, which it could not do before. Not request-free, and the comments say so rather than overclaiming: a screen showing a RegionPreset flag still pulls Noto Color Emoji, because the flags are emoji and no embedded font covers them. Closing that means bundling ~10MB or drawing the flags as assets; neither belongs here. All eight golden baselines move. The bundled Roboto is a different cut from the SDK's, which is what golden_toolkit was loading, so every glyph rasterizes a hair differently — layout, line breaks and positions are identical, checked against the diff rather than assumed. The new baselines are the more honest ones: they use the font that now ships. A release is one edit again. `BackupService._appVersion` was a second literal of pubspec's `version:`, kept in step by a test — which catches drift but does nothing about the failure that actually happened twice this week, a merge with no bump at all and so no deploy. It now reads a constant `build_version` generates from pubspec. A dev dependency producing a checked-in constant, not a runtime one: the reason package_info_plus was declined still holds, since nothing reads this back and the app should not carry a plugin to learn its own version. The test survives, re-aimed at a stale generated file, and was confirmed to fail when handed one. 2.23.0 covers this and the markdown emphasis work in #102, which merged without a bump and so is on main but was never published. Also adds tool/serve_web.mjs and a run-app skill. Running this headlessly needed four workarounds; two are now fixed above, and the other two are worth writing down rather than rediscovering: Flutter's engine throws RangeError from the JS Intl.Locale constructor when a headless browser reports "en-US@posix", and usePathUrlStrategy 404s on refresh under a plain static server. .claude/ stays ignored for local config, but skills are the opposite of machine-specific, so they are shared now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014kPZNZ5pCETb2mHRaz25Kz
5 tasks
IanHeinrich
added a commit
that referenced
this pull request
Aug 26, 2026
…103) Two files were fetched from Google on every page load: CanvasKit from gstatic.com and Roboto from fonts.gstatic.com. Both are Flutter Web defaults rather than choices anyone made here, and both sit badly in an app whose claim is that a CV never leaves the device — index.html already sets dartPdfJsBaseUrl locally for exactly that reason, and these two were simply missed. CanvasKit costs nothing to bring in-house: `flutter build web` already copies it into build/web, so those bytes were being deployed to Pages and then ignored in favour of a CDN round trip. The loader now points at them. Roboto is registered as a `fonts:` family from the four faces already bundled for the PDF, so CanvasKit finds it in the asset manifest instead of downloading it. The app now boots and renders with every non-origin request blocked, which it could not do before. Not request-free, and the comments say so rather than overclaiming: a screen showing a RegionPreset flag still pulls Noto Color Emoji, because the flags are emoji and no embedded font covers them. Closing that means bundling ~10MB or drawing the flags as assets; neither belongs here. All eight golden baselines move. The bundled Roboto is a different cut from the SDK's, which is what golden_toolkit was loading, so every glyph rasterizes a hair differently — layout, line breaks and positions are identical, checked against the diff rather than assumed. The new baselines are the more honest ones: they use the font that now ships. A release is one edit again. `BackupService._appVersion` was a second literal of pubspec's `version:`, kept in step by a test — which catches drift but does nothing about the failure that actually happened twice this week, a merge with no bump at all and so no deploy. It now reads a constant `build_version` generates from pubspec. A dev dependency producing a checked-in constant, not a runtime one: the reason package_info_plus was declined still holds, since nothing reads this back and the app should not carry a plugin to learn its own version. The test survives, re-aimed at a stale generated file, and was confirmed to fail when handed one. 2.23.0 covers this and the markdown emphasis work in #102, which merged without a bump and so is on main but was never published. Also adds tool/serve_web.mjs and a run-app skill. Running this headlessly needed four workarounds; two are now fixed above, and the other two are worth writing down rather than rediscovering: Flutter's engine throws RangeError from the JS Intl.Locale constructor when a headless browser reports "en-US@posix", and usePathUrlStrategy 404s on refresh under a plain static server. .claude/ stays ignored for local config, but skills are the opposite of machine-specific, so they are shared now.
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.
What and why
#98 removed
CvBullet.labeland said why in the model's own doc comment:That deleted the structural mechanism for emphasis in favour of this one.
Until now neither existed, so a bullet was a single uniform run with no way
to stress the metric or the product name in it — and anyone who tried the
obvious thing got literal asterisks in their exported PDF.
**bold**,*italic*and***bold italic***now work in every editablefield the CV prints: bullets, summary, references, hobbies, skills, language
names, the work-authorisation line, and every entry header. The four Roboto
faces were already loaded and sitting unused in
CvFontSet, so nothing newis embedded.
Two grammar rules earn their keep, and both are CV-specific. Underscores
are never emphasis, because
snake_caseand__init__are ordinary contentin a technical CV. And there is no intraword emphasis — a delimiter with a
letter or digit on both sides neither opens nor closes — because
A*A*Ais aUK A-level grade and a realistic
Education.grade. CommonMark permitsintraword
*and would silently italicise it.On ATS, this is neutral-to-positive rather than a risk. Bold is a font,
not markup: the extracted text is byte-identical either way, and italic
already ships today via
classic_centered'scompanytoken. It also removesjunk that reaches the text layer right now, since a typed
**deployment**currently puts literal asterisks in front of a keyword match. The one real
hazard is intraword emphasis splitting a word across two text-showing
operators, which some extractors rejoin with a space — and the A-level rule
above already prevents it. Contact addresses and URLs are excluded from
markup entirely for the same family of reasons.
The editors are the larger half, deliberately. Two icon buttons bolted to
a bare
TextFieldwould have made the feature reachable without making itgood. A
TextEditingControllersubclass now paints emphasis live — the wordbold, the markers dimmed — while leaving every character and offset in place,
so the caret and selection behave normally and a marker stays deletable.
Formatting is a bit toggle rather than string surgery, so bold-then-italic
composes instead of stripping a marker. Studio shows the Vault wording a
tailored field is diverging from, which previously required reverting to see.
Prose fields warn when they are running out of page and can open in a roomier
dialog, since a nested Studio row leaves roughly 300px to write a paragraph
through. The About-you panel is grouped instead of nine identical boxes.
Four defects in the editor pane are fixed along the way: a doubled field
label, a doubled value while editing, a second editor stealing focus from the
first, and an editing id outliving the row that owned it.
No new dependency.
package:markdowncan be configured to do the parsing—
parseInlineplus a hand-builtDelimiterSyntaxreaches the intraword rule— but its escaping collapses
\\server\share, and a conformance change in aminor version would silently re-render every CV. That is the same call this
repo already made for
package:intland CLDR month names, for the samereason.
How it was verified
dart format --output=none --set-exit-if-changed .flutter analyzeflutter test --exclude-tags=goldenlib/features/studio/widgets/, which has no golden coverage)That last box is not ticked and it matters — please read before merging.
This branch touches 14 files under
lib/features/studio/widgets/andrestructures the Vault's About-you panel, and none of it has been seen
running. It was built in a headless container with no display;
flutter build web --releasesucceeds, but that proves it compiles, not that it looksright. The live-styled field, the focus-revealed toolbar, the Vault-original
line, the length warning and the expand dialog all need eyes on them.
Also worth knowing: 889 tests pass, up from 688 on the branch point.
Goldens
update-goldens.yml, pulled, and the imagediff reviewed
Both fast paths — PDF and Flutter — rebuild the identical widget for text
containing no asterisk, and
example_vault.dartcontains none, so the treesare the same rather than merely equivalent. All 8 baselines pass unchanged.
vault_view_cv_defaults_open.pngshows as modified in this branch's diffagainst its original branch point, but that is #101's baseline arriving
through a merge — verified byte-identical to
origin/main's blob, not alocal regeneration.
The gap worth knowing: no golden renders a Vault editor panel with text
fields in it (
vault_view_cv_defaults_openopens the CV-defaults panel,which is dropdowns), so the About-you regrouping is genuinely uncovered.
The new stress test carries
@Tags(['golden'])? No — it is a pure parsertest with no rendering, so it is correctly untagged and runs in the
Testjob.
Anything else
Merged main twice while this was open, and the second one needed a fix git
could not flag. #101 moved
workAuthorizationout of the header's wrappedcontact run onto its own full-width line — correctly, since a
pw.Wrapcellcannot break and a long one ran off the margin. That structure wins and the
emphasis followed it there. But
photo_headerbuilds its own header, so #101gave it a second work-authorisation site, and that one auto-merged in clean
as a plain
pw.Text— silently outside this branch's coverage. Emphasis typedthere would have printed as asterisks in one template out of three with
nothing failing. It now has a per-template test; reverting that site fails it
by name.
The stress test found a bug in itself before it found one in the parser.
cv_markup_stress_test.dartruns CommonMark's own 152 emphasis andbackslash-escape inputs, CV-realistic nasties, and 20,000 seeded random
strings against seven invariants. The first version passed while a
deliberately re-introduced bug was present — stripping asterisks from both
sides before comparing is structurally blind to a dropped marker. The fix is
an accounting invariant: a matched pair takes the same count from each end, so
consumed asterisks can only ever be even. The mutant is then caught by the
fuzzer alone, at
fuzz[190]; no hand-written case found it.The split between the two test files was verified by breaking the parser four
ways rather than assumed. Dropped markers, broken coalescing and unspliced
leftovers fail the stress file; removing the intraword rule leaves every
invariant intact and fails
cv_markup_test.dartinstead. That is documentedin the file so the apparent gap does not get "fixed" later.
Deliberately left out.
CvVaultPruning._isBlankneeded no markupawareness — the plan assumed a field of
**renders as nothing, and buildingthe parser disproved it: unmatched delimiters are literal, and structurally no
non-empty string can strip to empty.
_TextOverride.normalizekeeps comparingraw strings on purpose, since
**foo**andfoorender differently and aregenuinely different overrides.
Follow-up not done here.
CvDraft.workAuthorizationOverrideexists and iswritten by the translation pass, but has no Studio hand-edit UI — a
pre-existing hole, noted rather than widened. And the reasoning for declining
package:markdowncurrently lives in this PR body rather than in the code.🤖 Generated with Claude Code
https://claude.ai/code/session_01XsmGo6QAsQ7RQpNxKUSWeU