Skip to content

Public API for renderers: placed markers, the template decision, helpers (#26) - #90

Merged
dvejsada merged 3 commits into
mainfrom
claude/laughing-feynman-bsvdkq
Sep 29, 2026
Merged

dvejsada merged 3 commits into
mainfrom
claude/laughing-feynman-bsvdkq

Conversation

@dvejsada

@dvejsada dvejsada commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Closes #26.

legaldown-render never re-derives a LegalDown rule, so it takes the validator's decisions from private modules. All of those imports sit in its validator_bridge.py, and they are why it pins legaldown-validator<0.3. Two of them are already broken against current main:

This PR makes what it needs public, so it can pin >=0.3,<0.4.

The two decisions, on ValidationResult

is_template: bool is the template decision validate_document already makes (§15.1).

placed_markers: list[PlacedMarker] holds every body marker that applies (§5.7, §15.3), in document order. It is filled from the markers the validator already found. PlacedMarker is frozen and keyword-only. Its fields:

Field What it holds
section, block, fragment Where the marker is: fragment fragment of block_fragments(block)
offset Its position in that fragment's text: text[offset:offset+len(source)] == source. This replaces the renderer's first-line search.
source The marker as written
identifier The #id that applies: "" for an include-only paragraph, whose #id is ignored (§12.2)
condition Its when= condition
field Which field of the block holds it: Literal["text", "suffix"], where suffix is for a lifted {{ref:}} or {{term:}}
item In a list, the item it marks. Items are numbered in pre-order, as list_fragments numbers them: an item comes before the items nested in it, and empty items count. An item can hold several fragments, so the fragment index alone cannot give this.
include_only True for an include-only paragraph's marker
line Its line (§16.9). None for a document built in code or changed since it was parsed.

A fragment holds at most one placed marker. A section's own marker stays Section.identifier / condition.

Identifiers and conditions are returned as written. An invalid one is still reported as a diagnostic, so callers should check result.is_valid first.

Public helpers

From legaldown:

  • is_template(document), with a clean one-argument signature. The internal form is _is_template.
  • is_drafting_note(block), true only for a quote block.
  • lex, Lexed, is_escaped.
  • block_fragments and list_fragments. They now return the named tuples Fragment(text, anchor) and ListFragment(text, anchor, items), which unpack exactly as before.
  • list_items.
  • PlacedMarker.

From legaldown.validator:

  • is_template, is_drafting_note, PlacedMarker.
  • The condition helpers: parse_condition, Condition, condition_problem, exclusive, Presence, ALWAYS.
  • The value checks were already there: is_valid_iso_date, is_valid_money_amount, is_positive_numeric, IDENTIFIER_RE, KNOWN_CURRENCIES.

With these, every private import in the renderer's validator_bridge.py and resolve/resolver.py has a public replacement:

  • _frontmatter_fields and text_fragments fed only its copy of the template formula, which is_template replaces.
  • find_markers and FoundMarker are replaced by placed_markers.
  • Quote.start, which the validator's Quote doesn't have, is replaced by is_drafting_note.
  • HTML_COMMENT_RE is needed only for the first-line search, which offset replaces.

Versioning (README, Python API)

The public API is what legaldown and legaldown.validator export in their __all__. Changes to it are listed in each GitHub release's notes. Before 1.0, a minor release may change it and a patch release does not. Other modules are internal. #34 tracks the constants that PactTrack still imports from them.

The README also gains rows for the two result fields and a table of the helpers.

Verification

Tests: the full suite passes (1922 passed, 40 skipped) and ruff is clean. The new tests/test_public_api.py covers:

  • multi-line paragraphs;
  • multi-paragraph list items (the item numbering), and two lists in one section;
  • empty items;
  • include-only paragraphs;
  • a preamble condition, placed only in a template;
  • a marker copy inside a comment;
  • out-of-place markers: in a table, a quote, a nested quote, mid-paragraph, and an item's second paragraph;
  • the suffix field after a lifted {{ref:}} and after a lifted {{term:}};
  • lines of list-item markers;
  • documents built in code, edited after parsing, and with string items;
  • each list walked only once;
  • is_template and is_drafting_note on their own, including a paragraph that is not a drafting note;
  • the named fragment tuples;
  • every public name being importable.

Parity over the spec corpus: over every .lgd in the LegalDown repository:

  • is_template matches;
  • placed_markers equals the validator's own placed findings;
  • each offset slices to its marker, and each line holds it.

That covers 30 markers, 20 of them in list items, and 24 templates.

Plan review:

  • item was added, because the fragment index alone does not locate a list-item marker.
  • is_drafting_note replaces Quote/block_quotes.
  • The versioning statement covers legaldown.validator.
  • Exports were trimmed to what the renderer uses.

Three code reviews:

  • A quadratic walk in _placed_markers is fixed, with a regression test: 2,000 marked items took 8.2 s and now take 0.17 s.
  • PlacedMarker became keyword-only and gained field.
  • is_drafting_note no longer accepts a paragraph that opens with [!DRAFTING].
  • Fragments are now named tuples.
  • Two surviving mutations (the per-list cache key, and field for a lifted term) are now caught by tests.
  • The last review compared the output with an independent walk of the model over about 4,000 random documents, and item and offset matched.

Follow-up (not in this PR)

legaldown-render needs to switch to this API after 0.3 is released:

  • drop validator_bridge.py;
  • use result.placed_markers, including their offsets and field, and result.is_template;
  • import the helpers from legaldown and legaldown.validator;
  • pin >=0.3,<0.4.

It also has to adapt to the 0.3 model: list items hold ListItem blocks, walked recursively and keyed by PlacedMarker.item, and paragraph text holds \n. The version bump to 0.3.0 comes with the release.

🤖 Generated with Claude Code

https://claude.ai/code/session_012pWb8FGHgaM3ZWbzsPiKsk

…ers (#26)

ValidationResult.is_template and .placed_markers hand back the two
decisions validate_document makes together (§15.1, §5.7, §15.3). Each
PlacedMarker says where the marker is (section, block, fragment, offset,
the list item it marks) and what applies (identifier, condition), with
its line. is_template, is_drafting_note, the lexer and the fragment
helpers are exported from legaldown; the condition helpers from
legaldown.validator. The README states what the public API is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012pWb8FGHgaM3ZWbzsPiKsk
_placed_markers read a list's fragments once per marker, quadratic in a
long list. PlacedMarker says which field of its block holds it (text,
or a lifted ref's or term's suffix), and takes keyword arguments only.
is_drafting_note is exported from legaldown.validator too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012pWb8FGHgaM3ZWbzsPiKsk
…ering

block_fragments and list_fragments return Fragment and ListFragment
named tuples, which unpack as before. is_drafting_note is True only for
a quote block. PlacedMarker.field is a Literal; its docs and the README
say items are numbered in pre-order and that identifiers and conditions
are as written. Tests cover two lists in one section and a lifted term.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012pWb8FGHgaM3ZWbzsPiKsk
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.

Public API for renderers: placed markers, the template decision, and the helpers they need

2 participants