You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
RenderCV Architecture Review — through the lens of the Development Philosophy
Scope: all 8.6k lines of src/, 10.4k of tests/, CI, scripts, toolchain. Five parallel deep-review agents + my own verification of every high-severity claim. Everything marked [verified] I reproduced by running code; everything with a file:line I read myself.
One framing correction first: the philosophy's Section 3 ("crush and burn") carries a pre-launch caveat — and RenderCV is launched (v2.8, PyPI, users, rendercv.com). So the compatibility boundary is the YAML contract + CLI surface. Everything behind that boundary — Python internals, module layout, the render pipeline — has no external consumers except your own web app, and can still be rebuilt freely. The findings below stay inside that boundary.
Verdict
The skeleton is genuinely good: one pipeline (YAML → pydantic → jinja → typst), one entry path (build_rendercv_dictionary_and_model), themes-as-data, disciplined typing culture, strong test infrastructure. The disease is in the middle of the pipeline: between the validated model and the Typst string there is no engineered representation — only a cascade of regex/string rewrites executed on a mutated copy of the validated model. Nearly every confirmed bug, every ty: ignore, every fallback, and a year of fix-commits trace back to that one missing piece of engineering. This is the Section 4 story told by your own git log: "Fix placeholder removal eating provided placeholders", "Fix cross-line emphasis interference" (#685), "Fix orphaned connector words", "Only catch full words in bold_keywords", "Fix multiline summary issues", "Don't fail on empty links" — ~12 commits, each locally reasonable, each patching one manifestation of the same absent structure.
Bugs found during this review (all in current main)
[verified] A section named interests crashes error reporting.pydantic_error_handling.py:24-32 filters pydantic union-branch names out of error locations by substring ("int", "str", "list", …). Any user key containing those substrings — interests, paintings, internships, distributed_systems — is silently dropped from the location path, and the coordinate lookup then dies with a raw KeyError: 0 instead of showing the validation table. The machinery built to display expected errors crashes with an impossible one (Section 5 inverted).
[verified] Typo'd keys in entries are accepted silently.compnay: TYPO-VALUE validates without complaint. Entries use extra="allow" (base.py:17-26, entries/bases/entry.py) — and the docstring states the reason: the renderer needs to setattr template-generated fields onto validated models. The renderer's scratchpad design bought its convenience by disabling typo detection in the most-edited region of every user's YAML — the exact feature the docs advertise with the strict-validation GIF.
[verified] Escaping placeholder corruption.markdown_parser.py:104-141 swaps Typst commands for RENDERCVTYPSTCOMMANDORMATH{i} and swaps back with str.replace. Index 1 is a string-prefix of index 10: with ≥11 commands/math in one text node, output is corrupted (#cmd1(x)0 instead of #cmd10(x)). Commit 2df9d226 fixed this same prefix-collision class in another placeholder system two weeks ago — the class keeps reappearing because the mechanism (flat-string sentinels) stays.
[verified] Empty markdown links fabricate https://example.com.markdown_parser.py:49. A user writing [text]() ships a CV linking to example.com, silently. History: 21543131 first made this raise — then 2e329bd4 "Don't fail on empty links" replaced the error with fabrication. A designed rejection was downgraded to a silent fallback (Section 2's exact definition).
Watch mode renders stale design/locale/settings.render_command.py:210-228 reads overlay file contents once at startup; the watch lambda closes over that arguments dict; run_rendercv re-reads only the main YAML (run_rendercv.py:140). So the watcher watches design.yaml, triggers on its change, and re-renders with the old design. (Code-verified; the closure is unambiguous.)
[verified] Test fixture type-dispatch is a shadowing tautology.tests/renderer/conftest.py:331-332 — the loop variable field_type shadows the parameter, so if field_type is field_type always matches the first dict entry and returns "https://example.com" for any unmatched type; the raise below is unreachable.
Dead Hypothesis profiles.tests/conftest.py:6-8 registers ci (200 examples) and dev (30), but nothing ever selects them — no HYPOTHESIS_PROFILE in test.yaml, no addopts. CI silently runs default (100). Speculative config that lies to the reader (Section 2).
Latent: explicit False overrides are dropped.rendercv_model_builder.py:149-151 merges CLI overrides with if value:, collapsing False into "unset" while the TypedDict promises bool | None. Today's CLI can't send False, so it's dormant — but the type contract lies to library callers (the web app).
The structural findings (the missing engineering, per Section 4)
1. There is no render IR — content is rewritten as flat strings, six times
Invariant: generated Typst is well-formed and faithfully represents user content; every placeholder is resolved exactly once. What happens: a string passes through keyword-bolding (regex) → per-line markdown parsing (markdown_to_typst, line-by-line specifically to patch cross-line interference, markdown_parser.py:158-192) → escaping with sentinel round-trips → locale phrase str.replace (entry_templates_from_input.py:141-146) → placeholder substitution → placeholder-cleanup regexes. Bugs #3 and #4 live here, plus the #OpenToWork ambiguity [verified]: prose hashtags pass through as Typst commands and fail compilation with an error pointing at generated code, not the user's YAML — a direct consequence of having two ways to express formatting (markdown AND raw-Typst passthrough) on one undifferentiated string (Section 1). The engineering: parse each content field once into a tree (the markdown library already gives you an ElementTree — you discard it back to a string). Keyword bolding, phrase expansion, and placeholder substitution become node operations on that tree; Typst/Markdown/HTML emission happens once at the end, escaping exactly once per text node at emission. Sentinels, round-trips, per-line parsing, and cleanup regexes all cease to exist rather than being fixed. Encode "resolved" in types: Template and RenderedText as distinct types, where only RenderedText can be emitted — the "unresolved placeholder" bug becomes uncompilable (Section 6).
2. The validated model doubles as a mutable render scratchpad
Invariant: a validated RenderCVModelis the user's input — one type, one meaning. What happens: process_model (model_processor.py:80) deep-copies the model, then mutates it; schema models carry renderer-only private attrs (_plain_name, _connections, _top_note, _footer); entry_templates_from_input.py:219-224setattrs arbitrary string-keyed render outputs onto entries; process_fields:187-189str()s any non-string field; Entry = EntryModel | str forces getattr(..., None)-plus-RenderCVInternalError duck typing (:150-196) and three ty: ignores; empty string is a sentinel for "absent" (:132-134). And the price of it all is Bug #2: extra="allow" on entries so the grafting works. The engineering: a separate frozen render IR — RenderSection/RenderEntry dataclasses built by a pure function schema model → IR. Schema models become frozen with extra="forbid"everywhere; typo detection returns; the deep-copy, the private attrs, the setattr grafting, the Entry | str ignores, and the double role all disappear. TextEntry becomes a real type instead of str. Templates receive the IR (typed context), not the schema model plus untyped **kwargs (templater.py:209-215).
3. Error machinery is string surgery over pydantic internals
Invariant: RenderCV classifies errors by what they are (structured type + location), never by prose. What happens: error_dictionary.yaml maps by substring-matching pydantic's English messages (pydantic_error_handling.py:89-92) — silent decay on any pydantic upgrade; unwanted_texts message replaces (:50-51); the substring location filter that causes Bug #1; hardcoded end_date/current_date special cases (:69-87); and design.py:37-55 routes built-in-vs-custom themes by parsing error["ctx"]["discriminator"] == "'theme'" — with embedded quote characters — from a caught ValidationError. That is a fallback keyed on a third-party error's internal formatting (Sections 2, 5, 6 at once). The engineering: (a) key friendly messages on error["type"] — pydantic's stable, documented contract — never on msg; (b) resolve locations structurally: a loc element is a union-branch artifact iff it is not a key/index present in the CommentedMap at that level — walk both together, no denylist; (c) theme routing becomes a look-before-leap decision: if design["theme"] in available_themes: validate built-in else load custom — the discriminator-parsing fallback is deleted. Note design.py currently raises through three mechanisms (PydanticCustomError, RenderCVInternalError, bare ValueError at :135) — one function, three error protocols.
4. Types are traded away at the theme/locale/section boundary
All 20 ty: ignores in src/ cluster on one pattern: types constructed at import time — reduce(or_, ...) unions (built_in_design.py:43, locale.py:44, section.py:43), pydantic.create_model section models (section.py:116-121), the variant generator, FontFamily Literal[*tuple(...)]. Worse, Design (design.py:152-155) is annotated as BuiltInDesign while its validator returns arbitrary custom-theme classes — the type is theater (Section 6's "worst position: looks safe"). And RenderCommand types placeholder templates (OUTPUT_FOLDER/NAME_..._CV.typ) as pathlib.Path — a value that is not a path claiming to be one. The engineering: the section models are a closed set — eight entry types that change only with a code change — so write the eight SectionWith*Entries classes statically; create_model buys nothing there. For themes/locales, keep YAML as the source of truth but generate the Python variant modules as a build step (just update-schema already establishes this exact committed-generated-artifact pattern, CI-enforced by test_generated_files.py). The type checker then sees real classes, IDEs autocomplete themes, and the runtime reduce magic goes away. Annotate Design honestly as ClassicTheme (every theme, custom ones included, is structurally a ClassicTheme).
5. Control flow runs through the UI; there is no library boundary
run_rendercv lives in cli/ and demands a concrete ProgressPanel; ProgressPanel.print_user_error/print_validation_errors (a display component) raise typer.Exit(code=1) (progress_panel.py:135,169); the watcher then suppresses typer.Exit to survive (watcher.py:29,61). Display, error policy, and process termination are braided across three layers. Meanwhile rendercv/__init__.py exports nothing — the web app must import deep internals with no defined contract (Section 7) — and __init__.py:7 runs warnings.filterwarnings("ignore", module="pydantic"), globally muting pydantic warnings in every host application. entry_point.py:16 catches any ImportError and prescribes "reinstall with [full]" — misdiagnosing genuine import bugs as installation errors (Section 5: a fallback masking which path failed). The engineering: a rendercv.render(inputs) -> RenderResult facade in the package root; it raises typed exceptions and reports steps through a small Protocol (two methods). The CLI is then: parse args → gather inputs → call facade → catch {RenderCVUserError, RenderCVUserValidationError} in one place (error_handler.py already exists for exactly this) → panel displays → Exit(1). The panel never raises; the watcher needs no suppression; watch mode calls "gather inputs" per render — which also deletes Bug #5 by construction rather than patching it. Photo downloading (model_processor.py:24-60 — network I/O, disk writes, and mutation of the original model, invoked from inside the templater, with a silent reuse-if-file-exists cache) moves to this input-gathering stage where I/O belongs.
Toolchain (Section 8)
Mostly exemplary — justfile + uv --frozen everywhere, CI calling the same recipes, generated artifacts (schema.json, examples, SKILL.md) CI-enforced against drift, the offline-wheel and pyodide tests, test_tests.py enforcing the tests-mirror-src convention. Remaining violations:
Two formatters: black (with unstable string_processing) runs locally in just format (justfile:9, with || true), but prek/CI runs only ruff-format. One axis, two tools, only one enforced. Pick ruff-format, delete black — or if black's string-splitting is the requirement, put black in prek and drop ruff-format. One must go.
.pre-commit-config.yaml pins ruff v0.15.7 and ty>=0.0.24 independently of the same pins in pyproject.toml dev deps — two version sources per tool.
test_generated_files.py:32-34 skips schema freshness on Windows ("generation differs") — a nondeterminism accepted as expected instead of engineered away.
Entry test data duplicated between tests/schema/models/cv/conftest.py and tests/renderer/conftest.py's return_value_for_field (which also has Bug #6).
Fix or delete the Hypothesis profiles (Bug 3rd party fonts are missing licenses #7): one line in test.yaml (HYPOTHESIS_PROFILE: ci) or remove the dead registrations.
What already embodies the philosophy (keep and defend)
Themes as YAML overrides on a single ClassicTheme model — one theme structure, N default-sets — is opinionation done right. One pipeline entry point with composable stages (rendercv_model_builder.py). The exception taxonomy (exception.py) maps exactly onto Section 5's expected/impossible split, and run_rendercv deliberately has no catch-all — real bugs propagate. Aggregated validation errors with YAML coordinates is a genuinely engineered UX. Reference-file testing with --update-testdata, 3×3 CI matrix, merged coverage with a 90% floor. Docstrings with "Why" sections. validation_context.py is small and clean. The #706 keyword-bolding fix added Hypothesis property tests — the right instinct; the properties now need a structure worth proving.
The big one — split schema from render IR (finding 2): restores typo detection (extra="forbid"), deletes the mutation/deep-copy/setattr layer, and is the prerequisite for finding 1.
Rebuild inline content on a tree (finding 1): kills the escaping/sentinel/per-line bug class that generates most of your fix traffic, including Bug Raise error only if status code is 404 #3.
Items 2+3 are one coherent re-engineering of the pipeline's middle — by Section 3 (still valid for internals), that rebuild is the expected mode, not an emergency. The YAML contract, themes, and CLI don't change; the reference-file test suite you already have is precisely the harness that makes this rebuild safe.
RenderCV Architecture Review — through the lens of the Development Philosophy
Scope: all 8.6k lines of
src/, 10.4k oftests/, CI, scripts, toolchain. Five parallel deep-review agents + my own verification of every high-severity claim. Everything marked [verified] I reproduced by running code; everything with afile:lineI read myself.One framing correction first: the philosophy's Section 3 ("crush and burn") carries a pre-launch caveat — and RenderCV is launched (v2.8, PyPI, users, rendercv.com). So the compatibility boundary is the YAML contract + CLI surface. Everything behind that boundary — Python internals, module layout, the render pipeline — has no external consumers except your own web app, and can still be rebuilt freely. The findings below stay inside that boundary.
Verdict
The skeleton is genuinely good: one pipeline (
YAML → pydantic → jinja → typst), one entry path (build_rendercv_dictionary_and_model), themes-as-data, disciplined typing culture, strong test infrastructure. The disease is in the middle of the pipeline: between the validated model and the Typst string there is no engineered representation — only a cascade of regex/string rewrites executed on a mutated copy of the validated model. Nearly every confirmed bug, everyty: ignore, every fallback, and a year of fix-commits trace back to that one missing piece of engineering. This is the Section 4 story told by your own git log:"Fix placeholder removal eating provided placeholders","Fix cross-line emphasis interference"(#685),"Fix orphaned connector words","Only catch full words in bold_keywords","Fix multiline summary issues","Don't fail on empty links"— ~12 commits, each locally reasonable, each patching one manifestation of the same absent structure.Bugs found during this review (all in current
main)interestscrashes error reporting.pydantic_error_handling.py:24-32filters pydantic union-branch names out of error locations by substring ("int","str","list", …). Any user key containing those substrings —interests,paintings,internships,distributed_systems— is silently dropped from the location path, and the coordinate lookup then dies with a rawKeyError: 0instead of showing the validation table. The machinery built to display expected errors crashes with an impossible one (Section 5 inverted).compnay: TYPO-VALUEvalidates without complaint. Entries useextra="allow"(base.py:17-26,entries/bases/entry.py) — and the docstring states the reason: the renderer needs tosetattrtemplate-generated fields onto validated models. The renderer's scratchpad design bought its convenience by disabling typo detection in the most-edited region of every user's YAML — the exact feature the docs advertise with the strict-validation GIF.markdown_parser.py:104-141swaps Typst commands forRENDERCVTYPSTCOMMANDORMATH{i}and swaps back withstr.replace. Index 1 is a string-prefix of index 10: with ≥11 commands/math in one text node, output is corrupted (#cmd1(x)0instead of#cmd10(x)). Commit2df9d226fixed this same prefix-collision class in another placeholder system two weeks ago — the class keeps reappearing because the mechanism (flat-string sentinels) stays.https://example.com.markdown_parser.py:49. A user writing[text]()ships a CV linking to example.com, silently. History:21543131first made this raise — then2e329bd4"Don't fail on empty links" replaced the error with fabrication. A designed rejection was downgraded to a silent fallback (Section 2's exact definition).render_command.py:210-228reads overlay file contents once at startup; the watch lambda closes over thatargumentsdict;run_rendercvre-reads only the main YAML (run_rendercv.py:140). So the watcher watchesdesign.yaml, triggers on its change, and re-renders with the old design. (Code-verified; the closure is unambiguous.)tests/renderer/conftest.py:331-332— the loop variablefield_typeshadows the parameter, soif field_type is field_typealways matches the first dict entry and returns"https://example.com"for any unmatched type; theraisebelow is unreachable.tests/conftest.py:6-8registersci(200 examples) anddev(30), but nothing ever selects them — noHYPOTHESIS_PROFILEintest.yaml, no addopts. CI silently runsdefault(100). Speculative config that lies to the reader (Section 2).Falseoverrides are dropped.rendercv_model_builder.py:149-151merges CLI overrides withif value:, collapsingFalseinto "unset" while theTypedDictpromisesbool | None. Today's CLI can't sendFalse, so it's dormant — but the type contract lies to library callers (the web app).The structural findings (the missing engineering, per Section 4)
1. There is no render IR — content is rewritten as flat strings, six times
Invariant: generated Typst is well-formed and faithfully represents user content; every placeholder is resolved exactly once.
What happens: a string passes through keyword-bolding (regex) → per-line markdown parsing (
markdown_to_typst, line-by-line specifically to patch cross-line interference,markdown_parser.py:158-192) → escaping with sentinel round-trips → locale phrasestr.replace(entry_templates_from_input.py:141-146) → placeholder substitution → placeholder-cleanup regexes. Bugs #3 and #4 live here, plus the#OpenToWorkambiguity [verified]: prose hashtags pass through as Typst commands and fail compilation with an error pointing at generated code, not the user's YAML — a direct consequence of having two ways to express formatting (markdown AND raw-Typst passthrough) on one undifferentiated string (Section 1).The engineering: parse each content field once into a tree (the
markdownlibrary already gives you an ElementTree — you discard it back to a string). Keyword bolding, phrase expansion, and placeholder substitution become node operations on that tree; Typst/Markdown/HTML emission happens once at the end, escaping exactly once per text node at emission. Sentinels, round-trips, per-line parsing, and cleanup regexes all cease to exist rather than being fixed. Encode "resolved" in types:TemplateandRenderedTextas distinct types, where onlyRenderedTextcan be emitted — the "unresolved placeholder" bug becomes uncompilable (Section 6).2. The validated model doubles as a mutable render scratchpad
Invariant: a validated
RenderCVModelis the user's input — one type, one meaning.What happens:
process_model(model_processor.py:80) deep-copies the model, then mutates it; schema models carry renderer-only private attrs (_plain_name,_connections,_top_note,_footer);entry_templates_from_input.py:219-224setattrs arbitrary string-keyed render outputs onto entries;process_fields:187-189str()s any non-string field;Entry = EntryModel | strforcesgetattr(..., None)-plus-RenderCVInternalErrorduck typing (:150-196) and threety: ignores; empty string is a sentinel for "absent" (:132-134). And the price of it all is Bug #2:extra="allow"on entries so the grafting works.The engineering: a separate frozen render IR —
RenderSection/RenderEntrydataclasses built by a pure functionschema model → IR. Schema models become frozen withextra="forbid"everywhere; typo detection returns; the deep-copy, the private attrs, thesetattrgrafting, theEntry | strignores, and the double role all disappear.TextEntrybecomes a real type instead ofstr. Templates receive the IR (typed context), not the schema model plus untyped**kwargs(templater.py:209-215).3. Error machinery is string surgery over pydantic internals
Invariant: RenderCV classifies errors by what they are (structured type + location), never by prose.
What happens:
error_dictionary.yamlmaps by substring-matching pydantic's English messages (pydantic_error_handling.py:89-92) — silent decay on any pydantic upgrade;unwanted_textsmessage replaces (:50-51); the substring location filter that causes Bug #1; hardcodedend_date/current_datespecial cases (:69-87); anddesign.py:37-55routes built-in-vs-custom themes by parsingerror["ctx"]["discriminator"] == "'theme'"— with embedded quote characters — from a caughtValidationError. That is a fallback keyed on a third-party error's internal formatting (Sections 2, 5, 6 at once).The engineering: (a) key friendly messages on
error["type"]— pydantic's stable, documented contract — never onmsg; (b) resolve locations structurally: a loc element is a union-branch artifact iff it is not a key/index present in theCommentedMapat that level — walk both together, no denylist; (c) theme routing becomes a look-before-leap decision:if design["theme"] in available_themes:validate built-in else load custom — the discriminator-parsing fallback is deleted. Notedesign.pycurrently raises through three mechanisms (PydanticCustomError, RenderCVInternalError, bareValueErrorat:135) — one function, three error protocols.4. Types are traded away at the theme/locale/section boundary
All 20
ty: ignores insrc/cluster on one pattern: types constructed at import time —reduce(or_, ...)unions (built_in_design.py:43,locale.py:44,section.py:43),pydantic.create_modelsection models (section.py:116-121), the variant generator,FontFamily Literal[*tuple(...)]. Worse,Design(design.py:152-155) is annotated asBuiltInDesignwhile its validator returns arbitrary custom-theme classes — the type is theater (Section 6's "worst position: looks safe"). AndRenderCommandtypes placeholder templates (OUTPUT_FOLDER/NAME_..._CV.typ) aspathlib.Path— a value that is not a path claiming to be one.The engineering: the section models are a closed set — eight entry types that change only with a code change — so write the eight
SectionWith*Entriesclasses statically;create_modelbuys nothing there. For themes/locales, keep YAML as the source of truth but generate the Python variant modules as a build step (just update-schemaalready establishes this exact committed-generated-artifact pattern, CI-enforced bytest_generated_files.py). The type checker then sees real classes, IDEs autocomplete themes, and the runtimereducemagic goes away. AnnotateDesignhonestly asClassicTheme(every theme, custom ones included, is structurally aClassicTheme).5. Control flow runs through the UI; there is no library boundary
run_rendercvlives incli/and demands a concreteProgressPanel;ProgressPanel.print_user_error/print_validation_errors(a display component)raise typer.Exit(code=1)(progress_panel.py:135,169); the watcher then suppressestyper.Exitto survive (watcher.py:29,61). Display, error policy, and process termination are braided across three layers. Meanwhilerendercv/__init__.pyexports nothing — the web app must import deep internals with no defined contract (Section 7) — and__init__.py:7runswarnings.filterwarnings("ignore", module="pydantic"), globally muting pydantic warnings in every host application.entry_point.py:16catches anyImportErrorand prescribes "reinstall with [full]" — misdiagnosing genuine import bugs as installation errors (Section 5: a fallback masking which path failed).The engineering: a
rendercv.render(inputs) -> RenderResultfacade in the package root; it raises typed exceptions and reports steps through a smallProtocol(two methods). The CLI is then: parse args → gather inputs → call facade → catch{RenderCVUserError, RenderCVUserValidationError}in one place (error_handler.pyalready exists for exactly this) → panel displays →Exit(1). The panel never raises; the watcher needs no suppression; watch mode calls "gather inputs" per render — which also deletes Bug #5 by construction rather than patching it. Photo downloading (model_processor.py:24-60— network I/O, disk writes, and mutation of the original model, invoked from inside the templater, with a silent reuse-if-file-exists cache) moves to this input-gathering stage where I/O belongs.Toolchain (Section 8)
Mostly exemplary —
justfile+uv --frozeneverywhere, CI calling the same recipes, generated artifacts (schema.json, examples, SKILL.md) CI-enforced against drift, the offline-wheel and pyodide tests,test_tests.pyenforcing the tests-mirror-src convention. Remaining violations:black(with unstablestring_processing) runs locally injust format(justfile:9, with|| true), but prek/CI runs onlyruff-format. One axis, two tools, only one enforced. Pick ruff-format, delete black — or if black's string-splitting is the requirement, put black in prek and drop ruff-format. One must go..pre-commit-config.yamlpinsruff v0.15.7andty>=0.0.24independently of the same pins inpyproject.tomldev deps — two version sources per tool.test_generated_files.py:32-34skips schema freshness on Windows ("generation differs") — a nondeterminism accepted as expected instead of engineered away.tests/schema/models/cv/conftest.pyandtests/renderer/conftest.py'sreturn_value_for_field(which also has Bug #6).test.yaml(HYPOTHESIS_PROFILE: ci) or remove the dead registrations.What already embodies the philosophy (keep and defend)
Themes as YAML overrides on a single
ClassicThememodel — one theme structure, N default-sets — is opinionation done right. One pipeline entry point with composable stages (rendercv_model_builder.py). The exception taxonomy (exception.py) maps exactly onto Section 5's expected/impossible split, andrun_rendercvdeliberately has no catch-all — real bugs propagate. Aggregated validation errors with YAML coordinates is a genuinely engineered UX. Reference-file testing with--update-testdata, 3×3 CI matrix, merged coverage with a 90% floor. Docstrings with "Why" sections.validation_context.pyis small and clean. The#706keyword-bolding fix added Hypothesis property tests — the right instinct; the properties now need a structure worth proving.Priority order
is not None), Bug Allow {start|end}_date to be less precise than a specific day #4 (empty link → validation error again), Bugs #6/3rd party fonts are missing licenses #7 in tests.extra="forbid"), deletes the mutation/deep-copy/setattrlayer, and is the prerequisite for finding 1.type-keyed structure; explicit theme routing (finding 3).ty: ignorecount from 20 toward ~0.Items 2+3 are one coherent re-engineering of the pipeline's middle — by Section 3 (still valid for internals), that rebuild is the expected mode, not an emergency. The YAML contract, themes, and CLI don't change; the reference-file test suite you already have is precisely the harness that makes this rebuild safe.