Skip to content

Renderer 0.2.0: adopt legaldown-validator 0.3.0, templates with answers, PyPI publishing - #2

Merged
dvejsada merged 19 commits into
mainfrom
claude/keen-ride-is254j
Sep 29, 2026
Merged

dvejsada merged 19 commits into
mainfrom
claude/keen-ride-is254j

Conversation

@dvejsada

@dvejsada dvejsada commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR moves the renderer onto legaldown-validator 0.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:

  • #49, #58, #60, #77: a model that follows CommonMark and GFM.
  • #71, #72, #81: list items and quotes that hold blocks, including headings.
  • #80: the validator's raw-html Warning.
  • #50: unique section numbers.
  • #55: assembly.
  • #85: diagnostics carry their line.
  • #87: a paragraph keeps its lines.
  • #90: the public API — placed markers, the template decision, and helpers.

Changes

Validator 0.3.0

  • The dependency is pinned to legaldown-validator>=0.3.0,<0.4.
  • Placed markers and the template decision come from the 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.
  • Imports now come from the public API: the lexer, list helpers, is_drafting_note, conditions and value checks. validator_bridge.py keeps only quote_content, the fence helpers and the answers-file reader (validator#93).
  • Line breaks: a hard break renders as <br>, and as a new line in text. A soft break is a newline in HTML and a space in text.
  • Line numbers: the CLI prints each validator diagnostic's line, as file:line:.

Block model

  • HTML blocks render nothing. Inline tags are dropped and their text is kept. raw-html is reported by the validator.
  • Tables keep their alignment.
  • Code blocks follow the validator's CommonMark reading of indented and fenced code.
  • Lists are built from the model's items, and each item holds blocks.
    • An item nested under 2.1(b) reads "2.1(b)(i)".
    • Empty items keep their place.
    • Sibling lists in one item continue their numbering.
  • Quotes hold the blocks the validator reads in them. A drafting note is is_drafting_note's decision.
  • Headings in items and quotes are not sections (§4.1). They render as a bold line.
  • Nesting limit: the renderer refuses (DocumentError) a document whose lists, quotes and inline formatting together nest past 100 levels.
  • Ambiguous references: a {{ref:}} whose designation another numbered unit also reads as gets a render-ref-ambiguous hint.

Section numbers

  • Numbers come from ValidationResult.sections.

Templates with answers (§15.8)

  • render(..., answers={...}) and --answers answers.yaml assemble the template with legaldown.assemble (§15.7), then render the assembled document.
  • It is refused (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.yml works as legaldown-validator publishes. A published GitHub Release triggers it:

    • build the sdist and wheel;
    • twine check --strict;
    • verify the tag matches __version__;
    • smoke-test the wheel with a real render;
    • upload to PyPI with Trusted Publishing.

    A manual run uploads to TestPyPI instead.

  • .github/PUBLISHING.md covers 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

  • Against validator 0.3.0 from PyPI: 387 passed, and ruff check . is clean. The count dropped from 561 because the tests that spied on is_template are gone: the renderer now reads ValidationResult.is_template itself.
  • The spec's assembly fixtures render exactly as their expected documents.
  • The built wheel passes twine check --strict, and it renders a document from a clean venv.
  • Golden files: soft line breaks now show as newlines in the HTML source, and the generator tag reads 0.2.0.

Known validator gaps

🤖 Generated with Claude Code

https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt

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
@dvejsada dvejsada changed the title Adopt the validator's CommonMark block model (legaldown-validator#49) Adopt the validator's CommonMark block model and section numbers (legaldown-validator#49, #50) Sep 24, 2026
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
@dvejsada dvejsada changed the title Adopt the validator's CommonMark block model and section numbers (legaldown-validator#49, #50) Follow legaldown-validator main: CommonMark model, section numbers, is_template, templates with answers Sep 25, 2026
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
@dvejsada dvejsada changed the title Follow legaldown-validator main: CommonMark model, section numbers, is_template, templates with answers Follow legaldown-validator main: CommonMark model, nested lists, section numbers, templates with answers Sep 27, 2026
- 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
…, #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
- 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 dvejsada changed the title Follow legaldown-validator main: CommonMark model, nested lists, section numbers, templates with answers Renderer 0.2.0: adopt legaldown-validator 0.3.0, templates with answers, PyPI publishing Sep 29, 2026
@dvejsada
dvejsada marked this pull request as ready for review September 29, 2026 21:07
@dvejsada
dvejsada merged commit 79cd33e into main Sep 29, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants