legaldown-validator 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ venv/
9
+ .pytest_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ coverage.xml
13
+
14
+ # Spec fixtures checkout used by the conformance harness in CI
15
+ .legaldown-spec/
16
+
17
+ # Editors / OS
18
+ .vscode/
19
+ .idea/
20
+ .DS_Store
@@ -0,0 +1,71 @@
1
+ # Conformance
2
+
3
+ `legaldown-validator` implements **Level 1 — Core** of the LegalDown specification (§16.2): parse
4
+ and validate a single document in memory.
5
+
6
+ It is verified against the specification's own
7
+ [fixtures corpus](https://github.com/ForLegalAI/LegalDown/tree/main/fixtures) — one case per
8
+ validation rule, paired with the diagnostic a conforming validator must produce. **57 of the
9
+ corpus's 95 rules are implemented, and every one the corpus can exercise at Core level passes.**
10
+
11
+ Seven implemented rules — `amend-def-override`, `amend-term-undefined`, `amend-term-unresolvable`,
12
+ `attachment-id-collision`, `attachment-id-duplicate`, `attachment-title-empty`, and
13
+ `attachment-unreferenced` — have fixtures that span several files or are marked for the Full level,
14
+ so this single-document harness skips them. They are covered by the unit tests in
15
+ `tests/test_spec_alignment.py` instead.
16
+
17
+ The specification (§16.5) requires an implementation never to silently skip a check it cannot
18
+ perform, so the remaining rules are named here. Most of them belong to conformance levels this
19
+ implementation does not claim — Rendering (§16.3) and Full (§16.4) — because they need to open
20
+ files other than the document itself.
21
+
22
+ ## Rules not implemented
23
+
24
+ | Group | Rules |
25
+ |---|---|
26
+ | Filesystem-dependent (Full, §16.4) | `amends-file-missing`, `attachment-file-missing`, `attachment-has-frontmatter`, `attachment-has-h1`, `attachment-anchor-duplicate`, `supersedes-file-missing`, `path-not-relative`, `path-outside-root` |
27
+ | Includes (Full, §16.4) | `include-file-missing`, `include-not-legaldown`, `include-cycle`, `include-has-frontmatter`, `include-has-h1`, `include-anchor-duplicate`, `include-heading-skip` |
28
+ | Bilingual sets (Full, §16.4) | `translation-file-missing`, `translation-hierarchy-mismatch`, `translation-anchor-mismatch`, `translation-def-mismatch`, `translation-language-set-mismatch`, `translation-implicit-id`, `translation-authoritative-absent` |
29
+ | Lexer-level grammar (§11.2–11.4) | `directive-malformed`, `directive-duplicate-param`, `directive-unknown-param`, `brace-stray`, `value-curly-quote`, `raw-html`, `anchor-misplaced` |
30
+ | Other | `frontmatter-absent`, `frontmatter-invalid-yaml` (reported by the CLI, not the validator), `anchor-lossy-slug`, `def-lossy-slug`, `definition-circular`, `definition-used-before-declaration`, `language-code-invalid`, `authoritative-not-declared`, `supersedes-title-empty` |
31
+
32
+ In practice this means multi-file processing is out of scope: includes, attachment file contents,
33
+ and bilingual document sets are not resolved or cross-checked. Single-document authoring, editing,
34
+ and CI validation are fully covered.
35
+
36
+ One id appears on both sides of that line. `attachment-file-missing` is defined as *the attachment
37
+ `file` path exists*, which needs the filesystem and is therefore unimplemented — but the validator
38
+ also emits that id when an attachment declares no `file` at all, which is visible in the document
39
+ itself. The specification provides no separate id for the absent key, so a diagnostic carrying
40
+ `attachment-file-missing` from this implementation always means the key is missing, never that the
41
+ path failed to resolve.
42
+
43
+ ## Checking this yourself
44
+
45
+ The conformance harness runs against a checkout of the specification repository:
46
+
47
+ ```bash
48
+ git clone https://github.com/ForLegalAI/LegalDown ../LegalDown
49
+ LEGALDOWN_FIXTURES_DIR=../LegalDown/fixtures pytest tests/conformance -q
50
+ ```
51
+
52
+ Cases for the rules above are skipped by name, so the 38 `not implemented` skips reproduce this
53
+ table one for one. The run reports 45 skips in total: the remaining seven are the implemented
54
+ rules named above, skipped as `multi-file case` or `requires conformance level full`. CI runs this
55
+ on every push and pull request.
56
+
57
+ ## Declaring conformance in code
58
+
59
+ The package exports what it targets, so consumers can assert it:
60
+
61
+ ```python
62
+ import legaldown
63
+
64
+ legaldown.SPEC_VERSION # "0.1" — specification version targeted
65
+ legaldown.CONFORMANCE_LEVEL # "core" — conformance level claimed
66
+ legaldown.__version__ # implementation version
67
+ ```
68
+
69
+ The CLI's JSON output repeats the first and last of these on every run, as `legaldown_spec` and
70
+ `validator_version`. The conformance level is not part of the JSON payload — read it from the
71
+ package.
@@ -0,0 +1,29 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ForLegalAI s.r.o.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ The LegalDown specification itself is a separate work, published at
26
+ https://github.com/ForLegalAI/LegalDown under Creative Commons Attribution 4.0
27
+ International (CC BY 4.0). This implementation is an independent work that
28
+ implements that specification; the specification permits implementations to be
29
+ released under any license their authors choose.
@@ -0,0 +1,308 @@
1
+ Metadata-Version: 2.5
2
+ Name: legaldown-validator
3
+ Version: 0.1.0
4
+ Summary: Reference parser and validator for the LegalDown legal document format
5
+ Project-URL: Homepage, https://github.com/ForLegalAI/legaldown-validator
6
+ Project-URL: Repository, https://github.com/ForLegalAI/legaldown-validator
7
+ Project-URL: Issues, https://github.com/ForLegalAI/legaldown-validator/issues
8
+ Project-URL: Specification, https://github.com/ForLegalAI/LegalDown
9
+ Author: ForLegalAI s.r.o.
10
+ Maintainer: ForLegalAI s.r.o.
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: contracts,document-format,legal,legaldown,legaltech,markdown,parser,validator
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: Legal Industry
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: Implementation :: CPython
25
+ Classifier: Topic :: Software Development :: Quality Assurance
26
+ Classifier: Topic :: Text Processing :: Markup
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.11
29
+ Requires-Dist: pyyaml>=6.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: build>=1.2; extra == 'dev'
32
+ Requires-Dist: pytest>=8.0; extra == 'dev'
33
+ Requires-Dist: ruff>=0.6; extra == 'dev'
34
+ Requires-Dist: twine>=5.1; extra == 'dev'
35
+ Description-Content-Type: text/markdown
36
+
37
+ <div align="center">
38
+
39
+ # legaldown-validator 📐
40
+
41
+ ### The reference implementation of [LegalDown](https://github.com/ForLegalAI/LegalDown)
42
+
43
+ **Parse, validate, and serialize LegalDown documents — from the command line or from Python.**
44
+
45
+ Targets specification **v0.1** · Every diagnostic carries a **stable rule id** · One dependency: PyYAML
46
+
47
+ </div>
48
+
49
+ ---
50
+
51
+ ## What it does
52
+
53
+ [LegalDown](https://github.com/ForLegalAI/LegalDown) is an open plain-text standard for legal
54
+ documents. This package is the toolkit for working with those documents in code:
55
+
56
+ 📥 **Parses** a `.lgd` file into a typed document model — frontmatter, sections, blocks, parties,
57
+ definitions, anchors.
58
+
59
+ ✅ **Validates** it against the specification's rule set (§15): broken cross-references, undefined
60
+ terms, duplicate identifiers, malformed party metadata, bad dates and money values, and more.
61
+
62
+ 📤 **Serializes** the model back to LegalDown source, so you can edit documents programmatically
63
+ and write them out again.
64
+
65
+ 🏷️ **Names every finding.** Each diagnostic carries the specification's stable rule id (§15.1)
66
+ alongside its severity and message, so you can suppress one check, escalate another, or gate a
67
+ build on exactly the rules you care about. Rule ids survive specification renumbering — they are
68
+ the part of a diagnostic that is safe to depend on.
69
+
70
+ ## Install
71
+
72
+ ```bash
73
+ pip install legaldown-validator
74
+ ```
75
+
76
+ Python 3.11 or newer. The distribution is named `legaldown-validator`; the import name is
77
+ `legaldown`:
78
+
79
+ ```python
80
+ import legaldown
81
+ ```
82
+
83
+ The package ships a `py.typed` marker, so type checkers use its annotations directly.
84
+
85
+ ## Quick start
86
+
87
+ Given a LegalDown document:
88
+
89
+ ```markdown
90
+ ---
91
+ title: Services Agreement
92
+ document_type: contract
93
+ sides:
94
+ - name: providers
95
+ label: Provider
96
+ parties:
97
+ - name: acme
98
+ label: Acme
99
+ type: legal_entity
100
+ legal_name: Acme Corporation
101
+ - name: clients
102
+ label: Client
103
+ parties:
104
+ - name: beta
105
+ label: Beta
106
+ type: legal_entity
107
+ legal_name: Beta Industries Inc.
108
+ language: en
109
+ ---
110
+
111
+ # Fees {#fees}
112
+
113
+ {{party: beta}} shall pay {{money: 5000}} under {{ref: payment-terms}}.
114
+ ```
115
+
116
+ Validate it:
117
+
118
+ ```bash
119
+ legaldown validate contract.lgd
120
+ ```
121
+
122
+ ```
123
+ contract.lgd: error: [ref-broken] Broken section reference: 'payment-terms'.
124
+ contract.lgd: warning: [money-missing-currency] Money directive without currency parameter.
125
+
126
+ 1 error(s), 1 warning(s), 0 info(s)
127
+ ```
128
+
129
+ One diagnostic per line, each prefixed with its rule id. A clean document reports
130
+ `No issues found.` and exits `0`.
131
+
132
+ Diagnostics go to **stdout**; the trailing summary and `No issues found.` go to **stderr**, so
133
+ `legaldown validate contracts/ > report.txt` captures the findings alone. `--quiet` drops the
134
+ summary entirely.
135
+
136
+ ## Command line
137
+
138
+ ```bash
139
+ legaldown validate contract.lgd # one document
140
+ legaldown validate contracts/ # a whole directory, recursively
141
+ legaldown validate --format json contract.lgd # machine-readable output
142
+ legaldown validate --ignore def-unreferenced doc.lgd # mute one rule
143
+ legaldown validate --warnings-as-errors doc.lgd # tighten a CI gate
144
+ ```
145
+
146
+ | Option | Effect |
147
+ |---|---|
148
+ | `--format {text,json}` | Output format (default `text`) |
149
+ | `--ignore RULE_ID` | Suppress a rule by its stable id — repeatable |
150
+ | `--warnings-as-errors` | Report warnings at error severity |
151
+ | `--strict` | Exit non-zero on any diagnostic, not just errors |
152
+ | `--quiet` | Drop the trailing summary line |
153
+
154
+ Directories are searched recursively for `*.lgd`, `*.legaldown`, and `*.legal.md`, and
155
+ `legaldown --version` reports the validator version.
156
+
157
+ **Exit codes:** `0` clean · `1` diagnostics found (errors, or any diagnostic under `--strict`) ·
158
+ `2` a file could not be read.
159
+
160
+ ### JSON output
161
+
162
+ `--format json` emits the structured shape described in §15.9 — ideal for CI annotations, editor
163
+ integrations, and dashboards:
164
+
165
+ ```json
166
+ {
167
+ "legaldown_spec": "0.1",
168
+ "validator_version": "0.1.0",
169
+ "diagnostics": [
170
+ {
171
+ "file": "contract.lgd",
172
+ "rule": "ref-broken",
173
+ "level": "error",
174
+ "message": "Broken section reference: 'payment-terms'."
175
+ }
176
+ ]
177
+ }
178
+ ```
179
+
180
+ ### In CI
181
+
182
+ ```yaml
183
+ - run: pip install legaldown-validator
184
+ - run: legaldown validate contracts/ --warnings-as-errors
185
+ ```
186
+
187
+ The exit code fails the job; the rule ids let you grant exceptions without turning whole checks
188
+ off.
189
+
190
+ ## Python API
191
+
192
+ Three functions cover the common path:
193
+
194
+ ```python
195
+ from legaldown import parse_document, validate_document, serialize_document
196
+
197
+ document = parse_document(open("contract.lgd").read(), filename="contract.lgd")
198
+ result = validate_document(document)
199
+
200
+ for diagnostic in result.diagnostics:
201
+ print(diagnostic.level, diagnostic.rule, diagnostic.message)
202
+
203
+ if result.is_valid: # no Error-level diagnostics
204
+ print(serialize_document(document))
205
+ ```
206
+
207
+ ### Working with the result
208
+
209
+ Validating a document builds the indices the checks need — section numbers, resolved definitions,
210
+ party display text, every inline value found in the body. `ValidationResult` hands all of it back,
211
+ so a renderer or a UI can reuse the work instead of re-deriving it:
212
+
213
+ | Attribute | Contents |
214
+ |---|---|
215
+ | `diagnostics` | `Diagnostic(rule, level, message)` — the authoritative record |
216
+ | `is_valid` | `True` when no Error-level diagnostic was reported |
217
+ | `errors` / `warnings` / `infos` | Message strings by severity |
218
+ | `rules(level=None)` | Set of rule ids present, optionally filtered by severity |
219
+ | `sections`, `section_lookup` | Numbered section index; resolves `{{ref:}}` targets |
220
+ | `definition_lookup`, `party_lookup`, `side_lookup`, `attachment_lookup` | Resolved display text |
221
+ | `inline_dates`, `inline_money`, `inline_durations`, `inline_fields`, `inline_placeholders` | Field-spec values found in the body |
222
+
223
+ ### Reading and editing the document model
224
+
225
+ `parse_document` returns a `Document` of plain dataclasses — `Metadata`, `Section`, `Block`,
226
+ `Side`, `Party`, `Attachment` — that you can inspect, edit, and write back out:
227
+
228
+ ```python
229
+ from legaldown import parse_document, serialize_document
230
+
231
+ document = parse_document(source)
232
+ document.metadata.governing_law = "Czech Republic"
233
+
234
+ with open("contract.lgd", "w", encoding="utf-8") as handle:
235
+ handle.write(serialize_document(document))
236
+ ```
237
+
238
+ `document_to_dict()` / `document_from_dict()` round-trip the model through JSON-friendly
239
+ structures, and `render_block()` renders a single block when you are driving your own layout.
240
+
241
+ One guarantee worth knowing: the parser is **faithful** — it never rewrites your input to make it
242
+ valid, so what you authored is exactly what the validator judges. The serializer, by contrast,
243
+ normalizes: frontmatter is re-emitted as canonical YAML and paragraphs are written as single
244
+ lines, so expect a formatting-normalized file rather than a byte-for-byte copy.
245
+
246
+ ## What gets checked
247
+
248
+ The full rule set with severities and examples lives in the specification (§15); this is the map:
249
+
250
+ | Area | Checks include |
251
+ |---|---|
252
+ | **Structure** | Heading depth and skipped levels, hardcoded section numbers, missing title |
253
+ | **Cross-references** | `{{ref:}}` targets that do not exist or point at an attachment |
254
+ | **Anchors** | Duplicate identifiers, malformed identifiers, auto-generated collisions |
255
+ | **Definitions** | Undefined `{{term:}}`, duplicate ids, missing quoted span, ambiguous quoting, unreferenced definitions |
256
+ | **Parties and sides** | Unknown `{{party:}}` / `{{side:}}`, malformed or duplicate names, invalid party types, minimum party and side counts, empty representatives |
257
+ | **Values** | Invalid dates, money without currency or with an unknown one, invalid durations and units, undeclared or reserved custom field types |
258
+ | **Placeholders** | Malformed ids, invalid or inconsistent types, placeholders in structural fields |
259
+ | **Attachments** | Undeclared `{{attach:}}`, duplicate or colliding ids, empty titles, unreferenced attachments |
260
+ | **Amendments** | Terms the amended original does not define, definition overrides, empty amendment titles |
261
+ | **Metadata** | Invalid document type, invalid dates, missing sides, issuer side requirements |
262
+
263
+ Each check reports at the severity the specification assigns it — Error, Warning, or Info.
264
+
265
+ ## Scope
266
+
267
+ This implementation claims **Level 1 — Core** (§16.2): everything above applies to a single
268
+ document, in memory, with no filesystem access beyond reading the file you point it at. That
269
+ covers authoring, editing, and CI validation of individual documents.
270
+
271
+ It is verified against the specification's own
272
+ [fixtures corpus](https://github.com/ForLegalAI/LegalDown/tree/main/fixtures) — every rule it
273
+ implements passes, bar seven whose fixtures span several files and are covered by unit tests
274
+ instead. The specification (§16.5) requires an implementation to be explicit about the
275
+ checks it does not perform, so those are listed in
276
+ [CONFORMANCE.md](https://github.com/ForLegalAI/legaldown-validator/blob/main/CONFORMANCE.md) rather than left to be discovered.
277
+
278
+ ## Development
279
+
280
+ ```bash
281
+ git clone https://github.com/ForLegalAI/legaldown-validator
282
+ cd legaldown-validator
283
+ pip install -e ".[dev]"
284
+ pytest
285
+ ```
286
+
287
+ ### Conformance suite
288
+
289
+ The fixtures corpus lives in the specification repository, so point the harness at a checkout:
290
+
291
+ ```bash
292
+ git clone https://github.com/ForLegalAI/LegalDown ../LegalDown
293
+ LEGALDOWN_FIXTURES_DIR=../LegalDown/fixtures pytest tests/conformance -q
294
+ ```
295
+
296
+ Cases for rules outside Core are skipped and named, so the run doubles as the coverage ledger in
297
+ [CONFORMANCE.md](https://github.com/ForLegalAI/legaldown-validator/blob/main/CONFORMANCE.md),
298
+ which accounts for every skip. CI runs it on every push and pull request.
299
+
300
+ Bug reports and pull requests are welcome in
301
+ [Issues](https://github.com/ForLegalAI/legaldown-validator/issues); questions about the format
302
+ itself belong in the specification repository's
303
+ [Discussions](https://github.com/ForLegalAI/LegalDown/discussions).
304
+
305
+ ## License
306
+
307
+ MIT — see [LICENSE](https://github.com/ForLegalAI/legaldown-validator/blob/main/LICENSE). The LegalDown specification itself is published separately under
308
+ CC BY 4.0; it permits implementations to choose their own license.