Renderer 0.2.0: adopt legaldown-validator 0.3.0, templates with answers, PyPI publishing - #2
Merged
Merged
Conversation
The validator now keeps HTML blocks, table alignment, every list marker, indented code, and the content after a Signature Block heading. The renderer follows its model: - An HTML block renders nothing; one that holds anything besides comments counts for the raw-html Warning (§8.6, §8.7) - The comment-across-blocks exception is removed: a comment block is now the validator's, and an unclosed <!-- inside a paragraph is text, as in CommonMark - Table columns keep their alignment (Block.align) - Indented code renders as code, without its four columns of indent - CONFORMANCE.md, the architecture doc, ADR 0007, and the roadmap drop the limitations #49 fixes; tests pin the new behaviour Needs the validator release that ships #49; it does not run on 0.2.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
The validator now keeps section numbers unique after a heading skip: a skipped level counts as 1, and numbers count from the shallowest heading (#, ###, ## -> 1, 1.1.1, 1.2). The renderer counted sections itself and disagreed. It now takes each section's number from ValidationResult.sections and only formats it, the n-th part with the n-th level format, so rendered numbers, references, and contents depth always agree with the validator's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
legaldown-validator#55 made the template decision one function, validator.core.is_template, used by validate_document and assembly alike. The renderer's copy of its formula, the frontmatter-field import it needed, and the test spy that checked the copy are gone; the tests now compare the renderer's markers-based answer with is_template alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
With an answers set, a template is assembled by legaldown.assemble (§15.7, legaldown-validator#55) and the assembled document rendered; without one, it renders as its template view. - RenderOptions.answers / render(..., answers=...) and --answers FILE (YAML or JSON) on the CLI - Refused (RenderRefused, exit 1) when the template has Errors, which §15.7.2 defines no output for; when the answers do (answer-invalid, answer-missing); or when the template needs other files, which a renderer below Full does not read (§17.6) - Assembly Warnings (answer-unknown) are reported with the rest; an unanswered blank stays and --final reports it - The conformance run renders each of the specification's assembly fixtures with its answers and checks the output equals the expected document's, in text and HTML; the multi-file case is refused - Docs: pipeline stage 2, CONFORMANCE, README, roadmap v0.2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
- Code blocks follow the validator's CommonMark helpers instead of a re-derivation: a fenced block's lines lose as much indentation as its opening fence had, and a fence indented four columns or more does not close it (legaldown.markdown: FENCE_OPEN_RE, closes_fence, dedent) - A section's rendered level is how many parts its number has, so its number format, heading tag, CSS class, and heading style agree when a document starts below level 1 or skips a level - The answers file is read by the validator's own reader, as `legaldown assemble` reads it - An assembly refused for template Errors carries every finding, the Warnings included; the redundant normalization of the output is gone - The template-decision tests compare with the value validate_document itself computes, not with is_template alone - pyproject.toml says the code needs the validator release after 0.2.0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
The validator now keeps nested lists: each listed item has its depth and the kind of the list it is in (models.listed_items). The builder nests them, an item of the other kind starting another list below the top level, as CommonMark does, and the resolver and writers, which already handled nesting, number and write them: an item nested under 2.1(b) is "2.1(b)(i)", no longer the flattened "2.1(c)". Also follows validator#60: a paragraph indented after a blank line is the list item's, so a marker there is misplaced and a reference to it broken, as the validator reports. Tests and golden files follow; CONFORMANCE.md, the roadmap (U1 done), and ADR 0007 drop the one-level limitation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
- Lists are grouped by the validator's list_runs, not a rule of the renderer's own, and the tree is built without recursion - A document nested so deeply that walking it exhausts the stack is a DocumentError, not an uncaught RecursionError - The text writer lines nested lists up with their item's text, as it already did the item's later paragraphs - Two gaps in the validator's model are filed and listed in CONFORMANCE.md: content after a nested list leaves its item (legaldown-validator#64), and a nested marker change stays one list (#65) - The roadmap's list of non-public validator imports names listed_items and list_runs; a stale test comment is rewritten Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
- A list of only empty items renders nothing, instead of raising a KeyError - Sibling lists nested in the same item and numbered in the same format share their counter, so no two of their items get the same designation - Nesting is limited to 100 levels of lists and quotes together, checked up front: a list by its deepest level, a quote by the quote markers that open its lines, before the validator's quote reading, whose cost grows with the depth. A deeper document is a DocumentError in well under a second, where it used to take a minute or more; the blanket RecursionError catch is gone, so a renderer bug is never blamed on the document - _nest copies only the items that hold nested lists and keeps no extra state; list() walks the listed items once - CONFORMANCE.md lists the nesting limit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
Nesting limit (MAX_NESTING = 100, now lists, quotes, and inline
formatting together):
- A list item's content is built as deep as the item, so lists and
quotes chained inside each other count together
- Inline formatting counts too, from the depth markdown-it records on
each token, before the tree is built: deep emphasis is a DocumentError,
not a RecursionError
- The quote bound counts markers however far apart, on the lexer's view,
so code lines do not count and spaced markers do; a deep quote is
refused before the validator's quote reading runs
List designations:
- Sibling lists in one item share a counter when their designations read
alike (counter style and reference form), not only when their whole
format is the same; a list reusing a counter starts its alternatives
afresh
- A {{ref:}} to an item whose designation another list's item shares
(each list starts again at its first number) renders with a
render-ref-ambiguous Warning; alternatives share a slot and never draw
it. No document in the specification corpus draws it.
Tests replace the wall-clock assertion with a check that the quote
reading never runs; CONFORMANCE.md states the limit and the Warning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
…, #72) The validator now gives list items that hold blocks (Block.items is a list of ListItem) and reads a quote's content as blocks (parser.quote_content); the flat items with depths the renderer nested (listed_items, list_runs) are gone. The builder follows: - A list is built item by item from its blocks, nested lists included. Items are numbered as the validator numbers them, in document order across nested lists, and a placed marker belongs to the innermost item its fragment is in (list_fragments) - A quote's content is quote_content's blocks, to the validator's quote depth; a drafting note is is_drafting_note's decision. The builder no longer re-reads container text with parse_document, and the quote-marker estimate and per-level block_quotes calls are gone - Empty items keep their place (validator#46) Fixed with it upstream and dropped from CONFORMANCE.md: content after a nested list (#64), nested marker changes (#65), indented code in quotes and items (#41), empty items (#46), tables without leading pipes (#44). A heading in a quote or item is now paragraph text in the model: filed as legaldown-validator#78 and listed. The validator caps lists at 64 levels and quotes at 16, so the renderer's 100-level limit now guards inline formatting and combinations; the tests follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
…eadings - The validator now reports raw HTML other than comments (raw-html, §8.7), once per block or text, as cmark-gfm reads it. The renderer's own count is gone, so the Warning is reported once; nothing is emitted, as before - A heading in a list item or a quote is a heading block in the model (fixes legaldown-validator#78). It is not a section (§4.1): it renders as a bold line, with no number and no anchor. A marker after it is text, as the validator reads it (§5.7) - CONFORMANCE.md, the architecture doc, and the roadmap follow Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
- render-ref-ambiguous warns only when another item reading the same can appear with the referenced one: each item's slot keeps its full presence, and alternatives or units under exclusive conditions (§15.4) never draw it. Lists in quotes and drafting notes, whose items the validator does not treat as units, are left out. Its advice no longer suggests numbered paragraphs, which do not change item designations - A drafting note's content is the validator's reading of the whole quote (quote_content), with the [!DRAFTING] marker taken off the first paragraph or heading, instead of a re-read of the text after the marker line: an indented line after the marker stays paragraph text, so its directives resolve - A list with no placed marker skips the list_fragments walk, and lists in quotes are not numbered for markers, as the docstring said - The counters for the ambiguity check are the counter objects themselves; the global key counter is gone - The nesting-limit comment, CONFORMANCE.md, and the resolver docstring say what the code does Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
- render-ref-ambiguous compares every alternative of the referenced unit,
not only the first, so the result no longer depends on their order
- Numbered paragraphs take part again: a paragraph and a list item
reading the same designation are flagged
- The Warning is a hint ("may"): units that can never appear together
are left out, but the condition the reference itself stands under is
not considered; CONFORMANCE.md says so
- A drafting note whose marker line the validator reads as code (indented
four columns) is the text after that line, as before, so the marker is
never shown
- The [!DRAFTING] marker text is defined in the renderer (§15.6) instead
of imported from the validator's private name
- The resolver's docstring lists the diagnostics it emits
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
- Sections, numbered paragraphs, and list items all record the
designation they read as, through one helper, so a list item or
paragraph that reads like a subsection ("1.1" in the continental
style) draws the hint too
- The referenced unit is the first to take the anchor, with its
alternatives (they share its number); a second use of the identifier
is another unit, so duplicates no longer hide each other
- The message no longer assumes two restarting lists: another section,
paragraph, or list item may read the same; refer to it in words or
number the parts apart
- Under the none scheme a numbered paragraph's label is its designation,
so the label and what {{ref:}} prints never differ
- _number_paragraph takes the counter and presence once; item_slots is
designation_slots; CONFORMANCE.md describes when the hint fires; tests
cover sections, duplicates, paragraphs under conditions, and an item
clashing only with a paragraph
Across the test and specification documents, only features.lgd under
the continental style draws the hint, and rightly: paragraph (2) of 2.1
and step 2 of its list both read "2.1(2)".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
…scheme
- Revert the none-scheme paragraph label override: labels follow the
style again ("(1)", not the heading text). A label that shows {section}
differs from the §13.3 designation {{ref:}} writes there, for items and
paragraphs alike; CONFORMANCE.md says so
- The hint's advice follows what clashes: two list items, "make them one
list, or refer to the item in words"; a heading under the none scheme,
"rename one"; otherwise, naming what else reads the same, "change the
style's numbering"
- A second use of an identifier is left to the validator's
anchor-duplicate, not repeated as a hint
- The referenced unit's slot is stored on its reference target when it
is registered, so the check no longer depends on the order units are
recorded in; the slot is a typed NamedTuple
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
- Pin legaldown-validator>=0.3.0,<0.4. - Placed markers and the template decision come from the ValidationResult (placed_markers, is_template): each marker's field, offset, and list item are the validator's, so the builder no longer searches for them or numbers list items itself. - Import the lexer, list helpers, is_drafting_note, conditions, and value checks from the public API. validator_bridge.py keeps only quote_content, the fence helpers, and the answers-file reader (validator#93). - A paragraph keeps its lines (validator#87): hard breaks render as <br> and in text as a new line; a soft break is a newline in HTML, a space in text. A marker ends its fragment, whatever line it is on. - The CLI prints a validator diagnostic's line, as file:line:. - Docs: #25, #26, #27 adopted; link reference definitions remain a limitation (validator#92). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
- Version 0.2.0. - .github/workflows/publish.yml, as legaldown-validator publishes: on a published GitHub Release, build the sdist and wheel, twine check, verify the tag matches __version__, smoke-test the wheel with a real render, and upload to PyPI with Trusted Publishing; a manual run uploads to TestPyPI. - .github/PUBLISHING.md: the one-time PyPI and GitHub setup, and how to cut a release. - README: install with pip install legaldown-render. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
dvejsada
marked this pull request as ready for review
September 29, 2026 21:07
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.
Summary
This PR moves the renderer onto
legaldown-validator0.3.0 and its new public API. It also adds rendering of templates with answers, and prepares the renderer's first PyPI release, 0.2.0.It builds on these validator changes, all released in 0.3.0:
raw-htmlWarning.Changes
Validator 0.3.0
legaldown-validator>=0.3.0,<0.4.ValidationResult(placed_markers,is_template). The validator gives each marker's field, offset and list item. The builder no longer searches for markers or numbers list items itself.is_drafting_note, conditions and value checks.validator_bridge.pykeeps onlyquote_content, the fence helpers and the answers-file reader (validator#93).<br>, and as a new line in text. A soft break is a newline in HTML and a space in text.file:line:.Block model
raw-htmlis reported by the validator.is_drafting_note's decision.DocumentError) a document whose lists, quotes and inline formatting together nest past 100 levels.{{ref:}}whose designation another numbered unit also reads as gets arender-ref-ambiguoushint.Section numbers
ValidationResult.sections.Templates with answers (§15.8)
render(..., answers={...})and--answers answers.yamlassemble the template withlegaldown.assemble(§15.7), then render the assembled document.RenderRefused) when the template or the answers have Errors, or when the template needs other files (§17.6).Release 0.2.0 and PyPI
The version is 0.2.0.
.github/workflows/publish.ymlworks aslegaldown-validatorpublishes. A published GitHub Release triggers it:twine check --strict;__version__;A manual run uploads to TestPyPI instead.
.github/PUBLISHING.mdcovers the one-time PyPI and GitHub setup.The README now says
pip install legaldown-render.Docs
CONFORMANCE.md, the architecture doc, the roadmap and ADR 0007 follow these changes.Testing
ruff check .is clean. The count dropped from 561 because the tests that spied onis_templateare gone: the renderer now readsValidationResult.is_templateitself.twine check --strict, and it renders a document from a clean venv.Known validator gaps
🤖 Generated with Claude Code
https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt