From 35b8278c9d4121d3de78e6ea72640c00dee9d916 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 09:13:46 +0000 Subject: [PATCH] Docs for 0.4.0: audit of README, CONFORMANCE, PUBLISHING, docstrings, CLI help MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every README Python example and shell command was run against the release candidate, every backticked API name resolved against the package, and every in-page link checked. Fixed: - README: the quick-start and JSON output showed the wrong lines (14/22 for findings on line 24) and one diagnostic of two; Scope said no file is read beyond the document (0.4.0 reads amended originals, attachments, and a template's files); the deprecation note now lists the deprecated importer type aliases; the __all__ trim is marked "Changed in 0.4.0"; slugify_identifier(value, …) and the keyword-only depth= of the quote readers; public names documented nowhere (SectionIndexEntry, ListItem, Amends, Representative, CustomField, AssemblyResult, AssemblyError) now are; Form.as_dict()'s fields. - CONFORMANCE: 85 of 113 rules (was 84: raw-html), the harness counts (234 passed, 39 skipped), answer rules point at Template.form, not the deprecated assemble. - PUBLISHING: the project has been on PyPI since 0.1.0. - Docstrings: no references to the internal quote_content or the deprecated assemble; HeadingSpan names {when=...}; legaldown and legaldown.validator module docstrings say what each holds. - CLI help: validate's streams and exit statuses, assemble refusing a template with template-rule errors, -i and --save-answers precisely. - pyproject description mentions template assembly. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz --- .github/PUBLISHING.md | 11 +++-- CONFORMANCE.md | 22 +++++---- README.md | 75 ++++++++++++++++++----------- pyproject.toml | 2 +- src/legaldown/__init__.py | 11 +++-- src/legaldown/assembly.py | 4 +- src/legaldown/cli.py | 21 ++++++-- src/legaldown/parser.py | 4 +- src/legaldown/positions.py | 2 +- src/legaldown/validator/__init__.py | 9 +++- src/legaldown/validator/core.py | 3 +- 11 files changed, 105 insertions(+), 59 deletions(-) diff --git a/.github/PUBLISHING.md b/.github/PUBLISHING.md index 8baf496..4a74c82 100644 --- a/.github/PUBLISHING.md +++ b/.github/PUBLISHING.md @@ -9,11 +9,12 @@ verifies the identity of the workflow run itself. ## One-time PyPI setup -This must exist before the first publish, and it must be done by a PyPI account that will own the -project. Because the project does not exist on PyPI yet, register it as a **pending** publisher. +This is in place: the project has been on PyPI since 0.1.0. It is recorded here in case the +publisher must be set up again, which takes an owner of the PyPI project. -1. Sign in to → **Your account** → **Publishing** → - *Add a new pending publisher*. +1. Sign in to → **Your projects** → `legaldown-validator` → **Manage** → + **Publishing** → *Add a new publisher* (for a project not yet on PyPI: **Your account** → + **Publishing** → *Add a new pending publisher*). 2. Fill in exactly: | Field | Value | @@ -54,7 +55,7 @@ built wheel by installing it and invoking the CLI, and uploads to PyPI. ## Dry run -Before a first real release, exercise the whole path against TestPyPI: +To rehearse a release, or after changing the workflow, exercise the whole path against TestPyPI: ``` Actions → Publish → Run workflow → target: testpypi diff --git a/CONFORMANCE.md b/CONFORMANCE.md index c743054..4c7c847 100644 --- a/CONFORMANCE.md +++ b/CONFORMANCE.md @@ -6,7 +6,7 @@ parse and validate a single document in memory. It also claims the **Assembly** It is verified against the specification's own [fixtures corpus](https://github.com/ForLegalAI/LegalDown/tree/main/fixtures) — one case per -validation rule, paired with the diagnostic a conforming validator must produce. **84 of the +validation rule, paired with the diagnostic a conforming validator must produce. **85 of the corpus's 113 rules are implemented, and every one the corpus can exercise at Core level passes**, at the line each case gives. @@ -17,7 +17,7 @@ holds it (`amends:` for a missing `amends.title`) or the frontmatter's first key not asserted at their given line: - The answer rules (`answer-invalid`, `answer-unknown`) point into the answers file, which - `assemble` receives already parsed, so its diagnostics name no line. + assembly receives already parsed (`Template.form(answers)`), so its diagnostics name no line. - `party-name-duplicate` gives line 10, the second side; this implementation reports line 12, the duplicate party's own entry, as `side-name-duplicate` and `representative-name-empty` point at theirs ([ForLegalAI/LegalDown#42](https://github.com/ForLegalAI/LegalDown/issues/42), @@ -195,11 +195,13 @@ LEGALDOWN_FIXTURES_DIR=../LegalDown/fixtures pytest tests/conformance -q Each case's expected rule is asserted at its expected line, where the case gives one (above). Cases for the rules above are skipped by name, so the 28 `not implemented` skips reproduce this -table one for one, except `ref-not-enumerated`, which has no fixture. The run reports 92 passed -and 38 skipped: eight of the other skips are the implemented rules named above, skipped as -`multi-file case` or `requires conformance level full`, and two are the `multi-file` assembly -case, which is marked Full (fixtures README, step 4) — `tests/test_assembly.py` assembles it with -a loader instead. Cases that need the final option run with it; cases that need an answers set +table one for one, except `ref-not-enumerated`, which has no fixture. Each fixture document is +also written back (`serialize`) and validated again, which must report the same diagnostics. The +run reports 234 passed and 39 skipped: eight of the other skips are the implemented rules named +above, skipped as `multi-file case` or `requires conformance level full`, two are the `multi-file` +assembly case, which is marked Full (fixtures README, step 4) — `tests/test_assembly.py` +assembles it with a loader instead — and one is the write-back of the `frontmatter-invalid-yaml` +case, whose frontmatter cannot be read. Cases that need the final option run with it; cases that need an answers set are assembled with it, and each assembly case is compared byte for byte with its expected output. CI runs this on demand (Actions → CI → Run workflow with the `conformance` input on, or the `ci` label on a pull request). @@ -210,9 +212,9 @@ the answer rules of §16.12 (`answer-invalid`, `answer-missing`, `answer-unknown structure assembly edits is recorded by the parser's own walk, so assembly and validation read a template the same way. `Template.questions` lists the questions a template asks; `Form.questions` those it reaches given an answers set, and `Form.unanswered` those still left open. §15.7.2 gives assembly a template that -validates without Errors, which assembly itself does not check; a `Template` refuses one with Errors -in the validator's template rules (`Template.problems`), and the deprecated `assemble` function does -not. +validates without Errors. Assembly does not require the whole of that, but a `Template` refuses a +template with Errors in the validator's template rules (`Template.problems`); the deprecated +`assemble` function does not. - A single-file template is assembled at Core, as §17.6 permits ("Core + Assembly"). - A template with include fragments or LegalDown attachment files is assembled when the caller diff --git a/README.md b/README.md index 5abc1d3..ece39cf 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ### The reference implementation of [LegalDown](https://github.com/ForLegalAI/LegalDown) -**Parse, validate, and serialize LegalDown documents — from the command line or from Python.** +**Parse, validate, and serialize LegalDown documents, and assemble templates — from the command line or from Python.** Targets specification **v0.2** · Every diagnostic carries a **stable rule id** · One dependency: PyYAML @@ -91,8 +91,8 @@ legaldown validate contract.lgd ``` ``` -contract.lgd:14: error: [ref-broken] Broken section reference: 'payment-terms'. -contract.lgd:22: warning: [money-missing-currency] Money directive without currency parameter. +contract.lgd:24: error: [ref-broken] Broken section reference: 'payment-terms'. +contract.lgd:24: warning: [money-missing-currency] Money directive without currency parameter. 1 error(s), 1 warning(s), 0 info(s) ``` @@ -203,8 +203,15 @@ integrations, and dashboards: "file": "contract.lgd", "rule": "ref-broken", "level": "error", - "line": 14, + "line": 24, "message": "Broken section reference: 'payment-terms'." + }, + { + "file": "contract.lgd", + "rule": "money-missing-currency", + "level": "warning", + "line": 24, + "message": "Money directive without currency parameter." } ] } @@ -258,14 +265,16 @@ not those it refers to in turn. A file that is not there, is not UTF-8, or has f if it had not been asked for: no diagnostic of its own. A document from `parse(text)` has no path, so there is nothing to read (`resolve=lambda path: None` does the same for a loaded one, to validate it without reading anything); give `validate` a `resolve=` function, from a relative path to the -file's text (or `None`), to read them from elsewhere — a database, an upload. `file_loader(directory)` -is the same function for files on disk, and the one `parse_template` takes as `resolve=` (`LoadFile`). +file's text (or `None`), to read them from elsewhere — a database, an upload. Such a function is a +`LoadFile`; `file_loader(directory)` makes one for files on disk, and `parse_template` takes one as +`resolve=` too. > **Deprecated:** `parse_document` is now `parse` (string; same arguments) or `load` (file), > `validate_document` is now `validate`, `serialize_document` is now `serialize` (text) or `save` -> (file), and the importer callbacks `import_definitions=` and `import_attachment_definitions=` are -> replaced by `resolve=`. They still work and raise a `DeprecationWarning`; they are deprecated -> since 0.4.0 and will be removed in 0.5.0. Every deprecation warning legaldown raises is a +> (file), and the importer callbacks `import_definitions=` and `import_attachment_definitions=` (and +> their types, `DefinitionsImporter` and `AttachmentDefinitionsImporter`) are replaced by `resolve=`. +> They still work, and calling one raises a `DeprecationWarning`; they are deprecated since 0.4.0 +> and will be removed in 0.5.0 (the template functions too: [Assembly](#assembly)). Every deprecation warning legaldown raises is a > `legaldown.LegaldownDeprecationWarning` (a `DeprecationWarning` subclass), so one filter catches > them all: `warnings.simplefilter("error", legaldown.LegaldownDeprecationWarning)` in code, or > `filterwarnings = ["error::legaldown.LegaldownDeprecationWarning"]` in pytest's configuration @@ -286,11 +295,11 @@ The public API is what the `legaldown` and `legaldown.validator` packages export model: loading, validating, saving and assembling documents, what that returns, and the `Document` dataclasses. `legaldown.syntax` and `legaldown.grammar` are the tooling API (see [Tooling API](#tooling-api)): reading source text the way the validator does, and the language's -constants and rules. The names that moved out of `legaldown.__all__` in 0.4.0 — the lexer, the -fragments, the definition readers, `render_block`, the dict factories and the like — still import -from `legaldown`, as the same objects and without a warning, so code written for earlier releases -keeps working; new code imports them from their home (the table at the end of -[Tooling API](#tooling-api)). Changes to the API are listed in the notes of each +constants and rules. **Changed in 0.4.0:** the lexer, the fragments, the definition readers, `render_block`, the dict +factories and the like moved out of `legaldown.__all__`, so `from legaldown import *` no longer +brings them. Imported by name they still import from `legaldown`, as the same objects and without +a warning, so code written for earlier releases keeps working; new code imports them from their +home (the table at the end of [Tooling API](#tooling-api)). Changes to the API are listed in the notes of each [GitHub release](https://github.com/ForLegalAI/legaldown-validator/releases); before 1.0 a minor release may change it, a patch release does not. Other modules are internal and may change in any release. @@ -312,7 +321,7 @@ itself is what was found, kept nowhere but in `diagnostics`: | `result.index.…` | Contents | |---|---| -| `sections`, `section_lookup` | Numbered section index; resolves `{{ref:}}` targets. Numbers count from the shallowest heading level, and a level a heading skips counts as 1 (`#`, `###`, `##` → 1, 1.1.1, 1.2), so no two sections share a number except alternatives and what they contain (§15.8); an entry's `alternative` is true for a section that is an alternative to its preceding sibling — the section just before it at its level under the same parent, with the same identifier, never present together with it (§15.4) — and so shares its number (§15.8) | +| `sections`, `section_lookup` | Numbered section index, as `SectionIndexEntry(title, identifier, path, level, number, alternative)`, in document order and by identifier (a paragraph's or item's anchor too, to its section); resolves `{{ref:}}` targets. Numbers count from the shallowest heading level, and a level a heading skips counts as 1 (`#`, `###`, `##` → 1, 1.1.1, 1.2), so no two sections share a number except alternatives and what they contain (§15.8); an entry's `alternative` is true for a section that is an alternative to its preceding sibling — the section just before it at its level under the same parent, with the same identifier, never present together with it (§15.4) — and so shares its number (§15.8) | | `definition_lookup`, `party_lookup`, `side_lookup`, `attachment_lookup` | Resolved display text | | `values` | The field-spec values the checks met, as written, as `InlineValues`: `dates`, `money`, `durations`, `fields`, `placeholders` (those of the frontmatter too; one with malformed arguments is not among them) | | `blanks` | The document's blanks, a template's or not, by placeholder id in order of first occurrence: `Blank(id, type, fixed, in_frontmatter, consistent)` — `type` the type the validator settled on: the effective type of the first occurrence with a valid one (§10.7, §15.2; `text` when none is written), or `None` when no occurrence has a usable type (each is written with an invalid one, or the id is a decision question's); `fixed` the currency of a money blank or the unit of a duration blank that every occurrence fixes (`""` when one fixes none, or the blank is not `consistent`); `consistent` false when its occurrences have different types or fix two currencies or units (placeholder-type-inconsistent); `in_frontmatter` whether one occurrence is in the frontmatter (§3.10) | @@ -326,8 +335,9 @@ the validator's own helpers ([Tooling API](#tooling-api)). ### Reading and editing the document model -`load` and `parse` return a `Document` of plain dataclasses — `Metadata`, `Section`, `Block`, -`Side`, `Party`, `Attachment` — that you can inspect, edit, and write back out: +`load` and `parse` return a `Document` of plain dataclasses — `Metadata` (with `Amends` for +`amends` and an object `supersedes`, and `Attachment`), `Section`, `Block` and `ListItem`, `Side`, +`Party` (with `Representative` and `CustomField`) — that you can inspect, edit, and write back out: ```python from legaldown import load, save @@ -481,7 +491,7 @@ slugify_identifier("日本語", fallback="") # "": nothing usable, as aga | `VALID_PLACEHOLDER_TYPES`, `DURATION_UNITS`, `VALID_DURATION_UNITS` | Placeholder types and duration units (§10); `DURATION_UNITS` is in the order the specification lists them | | `LIST_KINDS`, `MAX_SECTION_LEVEL`, `MAX_QUOTE_DEPTH`, `MAX_LIST_DEPTH` | Block kinds that are lists; the deepest heading level (5); how deep the parser reads quotes and lists | | `VALUE_QUESTION_TYPES`, `DECISION_QUESTION_TYPES`, `QUESTION_TYPES`, `DRAFTING_MARKER`, `FINAL_CHECK_RULES` | Template question types (§15.2); the drafting note's first line (§15.6); the rule ids of the final check (§15.9) | -| `slugify_identifier(text, *, fallback="section")`, `format_section_number` | The §5.3 identifier of a heading or term, and `fallback` where the text yields none (`""` tells that case apart); a dotted section number | +| `slugify_identifier(value, *, fallback="section")`, `format_section_number` | The §5.3 identifier of a heading or term text, and `fallback` where the text yields none (`""` tells that case apart); a dotted section number | | `is_valid_iso_date`, `is_valid_numeric`, `is_valid_money_amount`, `is_positive_numeric` | Value checks (§3.10, §10) | | `parse_condition` → `Condition`, `condition_problem`, `exclusive`, `Presence`, `ALWAYS` | Conditions (§15.3, §15.4): parse one, tell why one is invalid, tell whether two units can never appear together | | `choose_problem(directive, questions)` | Why a `{{choose:}}` is invalid (choose-invalid, §15.5): the first message `validate` records for it, or `None`; `ValueError` for a directive that is not a `{{choose:}}` or is malformed (`validate` reports that as directive-malformed) | @@ -511,7 +521,7 @@ for _section, _index, block in document.iter_blocks(): | `find_definition_anchors(text)` → `DefinitionAnchor` | Every `{{def:}}` in a text, with the quoted term it anchors and where that term starts (§7.2) | | `render_block(block)`, `render_item(item)` | The other way: a block written as LegalDown source, and a list item's content as written after its marker, as `serialize` writes them | | `Quote`, `block_quotes`, `is_drafting_note(block)` | The block quotes in a block and whether each is a drafting note; whether a quote block is one (§15.6) | -| `quote_blocks(block, depth=0)`, `drafting_note_blocks(block, depth=0)` | The blocks a block quote holds, as the validator reads them, as copies you may change; a drafting note's without its `[!DRAFTING]` marker (§15.6). `depth` is how many list items and quotes the quote is in, the count before entering it (a quote in a list item is at 1); past `MAX_QUOTE_DEPTH` the validator reads a quote as one text, so you get one paragraph of its text as written | +| `quote_blocks(block, *, depth=0)`, `drafting_note_blocks(block, *, depth=0)` | The blocks a block quote holds, as the validator reads them, as copies you may change; a drafting note's without its `[!DRAFTING]` marker (§15.6). `depth` is how many list items and quotes the quote is in, the count before entering it (a quote in a list item is at 1); past `MAX_QUOTE_DEPTH` the validator reads a quote as one text, so you get one paragraph of its text as written | | `code_content(block)` → `CodeContent(info, text, fenced)` | A code block read as CommonMark reads it: the info string, and the code without its fences or indentation | | `FRONTMATTER_RE`, `LINE_ENDING_RE`, `HTML_COMMENT_RE`, `FENCE_OPEN_RE`, `closes_fence`, `fence_end`, `dedent`, `indent_width`, `strip_text` | The Markdown rules the reading is built on: frontmatter, line endings, comments, fenced code, indentation | @@ -612,6 +622,12 @@ for diagnostic in result.diagnostics: print(diagnostic.level, diagnostic.rule, diagnostic.message) ``` +`form.assemble()` returns an `AssemblyResult`: `ok` (no Error), `output`, `files` (an emptied +fragment is `""`, written as zero bytes) and `diagnostics`; nothing is assembled while an Error +stands, and `output` and `files` are then empty. `AssemblyError` is raised, rather than a guess +made, if the template's source cannot be mapped onto its parsed structure — never expected for a +template the parser reads, so report it if you meet it. + A template is read once; a form is a snapshot of the interview over it, so a front end asks for a new form after every answer — a decision opens or closes the questions under it. `parse_template(text)` (or `Template(text)`, which takes the same `resolve=`) is the same for source text; `template.path` is @@ -633,7 +649,7 @@ neither do the questions: a `Question` is a fixed value, a copy of the template' every form of the template shares (treat its `default` and `choices` as read-only). **Asking a person.** A question can read what a person types, so a terminal, a web form or an agent -need not know the shapes `assemble` takes (money is `{amount, currency}`, a boolean is a boolean): +need not know the shapes an answers set holds (money is `{amount, currency}`, a boolean is a boolean): ```python answers, skipped = {}, set() @@ -681,8 +697,9 @@ form = template.form(answers) **The form as data.** `form.as_dict()` is the form as JSON-ready data for a web form, a service or an agent: `ready`, `complete`, the template's `problems`, the `diagnostics` about the answers (each with the `question` it is about), and `questions` — those reached first, in order, then the others — each with -`id`, `type`, `label`, `prompt`, `reached`, `blocking`, `state` (`answered`, `default`, `invalid`, -`unanswered`), `answer`, `default`, `problem`, `hint` in words and `accepts` as data: the words of a +`id`, `type`, `label`, `prompt`, `declared`, `choices`, `currency`, `unit`, `reached`, `blocking`, +`state` (`answered`, `default`, `invalid`, `unanswered`), `answer`, `default`, `problem`, `hint` in +words and `accepts` as data: the words of a boolean, the choices, the currency or unit the placeholders fix, the units there are. `question.to_text(answer)` is what a person would type for an answer (`yes`, `5000 EUR`, `30 D`), to show a default or fill in an input; `answer_text` and `default_text` hold it in the data. @@ -725,14 +742,16 @@ checks and what it does not. ## Scope This implementation claims **Level 1 — Core** (§17.2) and the **Assembly** capability (§17.6): -everything above applies to a single document, in memory, with no filesystem access beyond -reading the file you point it at, and the files an assembled template includes when you supply -them. That covers authoring, editing, CI validation, and assembly of individual documents. +everything above applies to a single document. Beyond the file you point it at, validation reads +only the definitions declared by the document it amends and by its LegalDown attachment files +(§7.5, §12.4), and assembly the include fragments and LegalDown attachment files of a template — +each from beside the document, never from outside its directory, or through `resolve=`. That +covers authoring, editing, CI validation, and assembly of individual documents. It is verified against the specification's own [fixtures corpus](https://github.com/ForLegalAI/LegalDown/tree/main/fixtures) — every rule it -implements passes, bar seven whose fixtures span several files and are covered by unit tests -instead. The specification (§17.5) requires an implementation to be explicit about the +implements passes, bar eight whose fixtures span several files or are marked for the Full level, +and are covered by unit tests instead. The specification (§17.5) requires an implementation to be explicit about the checks it does not perform, so those are listed in [CONFORMANCE.md](https://github.com/ForLegalAI/legaldown-validator/blob/main/CONFORMANCE.md) rather than left to be discovered. @@ -764,7 +783,7 @@ LEGALDOWN_FIXTURES_DIR=../LegalDown/fixtures pytest tests/conformance -q Cases for rules outside Core are skipped and named, so the run doubles as the coverage ledger in [CONFORMANCE.md](https://github.com/ForLegalAI/legaldown-validator/blob/main/CONFORMANCE.md), -which accounts for every skip. CI runs it with the rest of the checks, on demand (below). +which accounts for every skip. CI runs it with the rest of the checks, on demand (above). Bug reports and pull requests are welcome in [Issues](https://github.com/ForLegalAI/legaldown-validator/issues); questions about the format diff --git a/pyproject.toml b/pyproject.toml index c290d4b..1a193e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "legaldown-validator" # Single-sourced from src/legaldown/__init__.py so the package and the # distribution can never disagree about the version. dynamic = ["version"] -description = "Reference parser and validator for the LegalDown legal document format" +description = "Reference parser, validator and template assembler for the LegalDown legal document format" readme = "README.md" requires-python = ">=3.11" license = "MIT" diff --git a/src/legaldown/__init__.py b/src/legaldown/__init__.py index 57bf883..24d5173 100644 --- a/src/legaldown/__init__.py +++ b/src/legaldown/__init__.py @@ -1,8 +1,13 @@ """legaldown — Reference implementation of the LegalDown document format. -Parse, serialize, and validate LegalDown documents. Every diagnostic carries -the specification's stable rule id (§16.1), so tooling can filter, suppress, -or escalate individual checks. Only external dependency: PyYAML. +Parse, serialize, and validate LegalDown documents, and assemble templates +(§15.7). Every diagnostic carries the specification's stable rule id (§16.1), +so tooling can filter, suppress, or escalate individual checks. Only +external dependency: PyYAML. + +``legaldown`` is the workflow and the document model; tools that read +LegalDown source the way the validator does use ``legaldown.syntax`` and +``legaldown.grammar``. Quick start:: diff --git a/src/legaldown/assembly.py b/src/legaldown/assembly.py index 70bb8a9..4e1698f 100644 --- a/src/legaldown/assembly.py +++ b/src/legaldown/assembly.py @@ -106,7 +106,7 @@ class AssemblyError(ValueError): @dataclass(slots=True) class AssemblyResult: - """What :func:`assemble` produced. + """What assembling a template produced (``Form.assemble``). ``output`` is the assembled template file; ``files`` the assembled include fragments and LegalDown attachment files that remain, by relative path — an @@ -254,7 +254,7 @@ def to_text(self, answer: Any) -> str: def from_text(self, text: str) -> Any: """The answer that *text*, as a person types it, gives to this question: - the shape ``assemble`` takes, which :meth:`problem` accepts — or ``None`` + the shape an answers set holds (§15.7.1), which :meth:`problem` accepts — or ``None`` for no answer (the empty text: the default applies, or the blank stays). Surrounding spaces are dropped. Strict, and not locale-aware: diff --git a/src/legaldown/cli.py b/src/legaldown/cli.py index e91a018..f2f579e 100644 --- a/src/legaldown/cli.py +++ b/src/legaldown/cli.py @@ -11,6 +11,8 @@ legaldown validate --ignore def-unreferenced --warnings-as-errors doc.lgd legaldown validate --final signed-contract.lgd legaldown assemble template.lgd --answers answers.yaml -o out/ + legaldown assemble template.lgd -i --answers a.yaml --save-answers a.yaml -o out/ + legaldown questions template.lgd --answers answers.yaml --format json """ from __future__ import annotations @@ -478,13 +480,21 @@ def build_parser() -> argparse.ArgumentParser: prog="legaldown", description=( "LegalDown reference validator " - f"(specification {SPEC_VERSION}, Core conformance level)." + f"(specification {SPEC_VERSION}, Core conformance level, Assembly capability)." ), ) parser.add_argument("--version", action="version", version=f"legaldown {__version__}") sub = parser.add_subparsers(dest="command", required=True) - validate = sub.add_parser("validate", help="Validate LegalDown documents.") + validate = sub.add_parser( + "validate", + help="Validate LegalDown documents.", + description=( + "Validate LegalDown documents. Diagnostics go to standard output, the summary to " + "standard error. Exit status 0 when clean, 1 when diagnostics are found (errors, " + "or any with --strict), 2 when a file cannot be read." + ), + ) validate.add_argument( "paths", nargs="+", @@ -532,7 +542,8 @@ def build_parser() -> argparse.ArgumentParser: help="Assemble a template with an answers set (§15.7).", description=( "Assemble a template with an answers set (§15.7). Include fragments and " - "LegalDown attachment files are read relative to the template." + "LegalDown attachment files are read relative to the template. A template with " + "Errors in the validator's template rules is refused. Exit status as for validate." ), ) assemble_cmd.add_argument("template", help="The template file.") @@ -556,13 +567,13 @@ def build_parser() -> argparse.ArgumentParser: help=( "Ask, in the terminal, for the answers --answers does not give, as the " "questions are reached (prompts go to standard error). Without a terminal " - "it lists what must still be answered and exits 1." + "it lists what must still be answered and exits 1, or assembles if nothing must be." ), ) assemble_cmd.add_argument( "--save-answers", metavar="FILE", - help="Write the answers used, with those typed, to FILE (YAML), also after an interruption.", + help="Write the answers used, with those typed, to FILE (YAML), also after an interruption or a failure.", ) assemble_cmd.set_defaults(func=_run_assemble) diff --git a/src/legaldown/parser.py b/src/legaldown/parser.py index cf0dd11..7040f21 100644 --- a/src/legaldown/parser.py +++ b/src/legaldown/parser.py @@ -1369,8 +1369,8 @@ def _read_quote_content(text: str, depth: int = 0) -> tuple[tuple[Block, ...], t def quote_blocks(block: Block, *, depth: int = 0) -> list[Block]: - """The blocks the block quote *block* holds, as the validator reads them - (``quote_content``): each block read afresh, free to change, since the + """The blocks the block quote *block* holds, as the validator reads them: + each block read afresh, free to change, since the validator keeps its own reading of a quote's content. A heading in one is a ``heading`` block, not a section, and no directive is lifted into block fields (§4.1). *depth*: how many list items and quotes *block* is diff --git a/src/legaldown/positions.py b/src/legaldown/positions.py index 703677e..7472e0b 100644 --- a/src/legaldown/positions.py +++ b/src/legaldown/positions.py @@ -111,7 +111,7 @@ class ItemSpan: class HeadingSpan: """Where a heading lies in the file: lines ``[start, end)``, and the line its marker is on (an ATX heading's line, or a setext heading's last text - line, where a ``{#id}`` or ``{if:}`` marker is written, §5.2).""" + line, where a ``{#id}`` or ``{when=...}`` marker is written, §5.2, §15.3).""" start: int end: int diff --git a/src/legaldown/validator/__init__.py b/src/legaldown/validator/__init__.py index b754d0b..f2985ea 100644 --- a/src/legaldown/validator/__init__.py +++ b/src/legaldown/validator/__init__.py @@ -1,4 +1,11 @@ -"""legaldown.validator — Document validation and structural analysis.""" +"""legaldown.validator — Document validation and structural analysis. + +``validate``, ``is_template`` and the result types, which ``legaldown`` +exports too; and the condition, value and identifier rules and constants +the checks use, whose home for tools is ``legaldown.grammar`` (the same +objects). Part of the supported API, like ``legaldown``; its submodules are +internal. +""" from __future__ import annotations from .conditions import ALWAYS, Condition, Presence, condition_problem, exclusive, parse_condition diff --git a/src/legaldown/validator/core.py b/src/legaldown/validator/core.py index 8ae7f10..821e5a8 100644 --- a/src/legaldown/validator/core.py +++ b/src/legaldown/validator/core.py @@ -69,7 +69,8 @@ ) from .units import FoundMarker, Units, find_markers, marker_matches, own_presence -# Type aliases for the optional definitions-import callbacks. +# Type aliases for the definitions-import callbacks: deprecated since 0.4.0, +# removed in 0.5.0, with the callbacks (``validate(resolve=)`` replaces them). DefinitionsImporter = Callable[[str, str], dict[str, str] | None] AttachmentDefinitionsImporter = Callable[[str], dict[str, str] | None]