Skip to content

0.4.0: simpler API — load/validate/DocumentIndex, Template/Form interview, questions and assemble -i - #96

Merged
dvejsada merged 30 commits into
mainfrom
claude/jolly-johnson-e3yr9u
Oct 2, 2026
Merged

dvejsada merged 30 commits into
mainfrom
claude/jolly-johnson-e3yr9u

Conversation

@dvejsada

@dvejsada dvejsada commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A step-by-step simplification of the external Python API, in three parts. Every old entry point still works and warns (DeprecationWarning), to be removed in 0.5.0. The version is bumped to 0.4.0.

1. Opening a document

  • load(path) opens a document (UTF-8, BOM and line endings handled) and sets Document.path; parse(text) is the string form.
  • Deprecated: parse_document.

2. Validation: input and output

  • validate(document, *, final=False, resolve=None). The amended original's and the LegalDown attachment files' definitions are read from beside document.path (sandboxed to its directory, one level deep, never the document itself). resolve= replaces the two importer callbacks and has the same shape as assembly's LoadFile. New shared module files.py (LoadFile, file_loader, relative_path).
  • ValidationResult is diagnostics + index. The analysis moved to result.index (DocumentIndex: sections, the lookups, values, is_template, placed_markers). errors/warnings/infos are derived from diagnostics; recording methods and used_terms moved to an internal recorder.
  • Deprecated: validate_document, import_definitions=, import_attachment_definitions=.

3. Assembly as an interview

  • load_template(path) / parse_template(text, resolve=) read a template once. Template.form(answers) is a frozen snapshot: questions (reached, includes read where their {{include:}} is), unanswered, blocking, unused, diagnostics, ready, complete, problem(id), assemble(), as_dict().
  • Question (now a frozen value) gets problem(answer), from_text(text), to_text(answer), hint and accepts.
  • Template.coerce(answers) and load_answers(path).
  • CLI: legaldown questions (text or JSON, --check), legaldown assemble -i (asks in the terminal; prompts on stderr) and --save-answers FILE (atomic, keeps the file's permissions, never the template itself).
  • Deprecated: assemble, template_questions, needed_questions.

Behaviour changes to review

  • legaldown validate now reads the amended original and attachment files beside a document, so documents with amends or attachments can gain real findings (amend-def-override, def-duplicate-id; a missing term moves from Info amend-term-unresolvable to Error amend-term-undefined). Noted in the README as "Changed in 0.4.0".
  • legaldown assemble and the new Template refuse a template with Errors in the validator's template rules (§15.7.2 gives assembly a valid template); the deprecated assemble function does not. The CLI also puts the shapes of the answers file right (5000 EUR, no, trimmed text; fee: 5000 where the placeholders fix the currency), which were answer-invalid before.
  • What a person types as a text answer may not hold a control character (a tab is fine) or a lone surrogate (Question.from_text); answer_problem itself is exactly §15.7.1.
  • A LoadFile/resolve= loader is only asked for paths within the document's directory (§2.3), including by the deprecated assemble.
  • The new API asks the questions of an included fragment where its {{include:}} is; the deprecated functions ask them after the body.
  • ValidationResult and Question changes are breaking for code that read the old fields or built a result positionally (it is now keyword-only). This is a deliberate hard break, documented in the README.

Review history

Each stage was reviewed adversarially by an Opus subagent against the code, with probes (including a real pty for -i), and the findings fixed before the next stage. A final independent Opus review of the whole PR ran differential checks of legaldown validate --format json against main on every spec fixture (only the amendment cases differ, as documented), found one blocking and several important issues, and confirmed their fixes. The deprecated functions run on the old internal code paths, and tests/test_assembly.py runs every one of its tests twice, through the functions and through Template/Form, comparing ok, output, files and rule ids.

Test plan

  • python -m pytest -q -W error::DeprecationWarning — 1976 passed, 14 skipped
  • With the specification fixtures corpus (LEGALDOWN_FIXTURES_DIR, LegalDown v0.2) — 2409 passed, 40 skipped; the Template/Form path (ungated, and the public door with the template rules on) assembles all assembly fixtures byte for byte
  • ruff check src tests
  • CI: lint, tests on Python 3.11 / 3.12 / 3.13, build, specification conformance

🤖 Generated with Claude Code

https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz

claude added 25 commits October 2, 2026 10:19
…(deprecated, removed in 0.5.0)

load() reads a file as UTF-8 and sets Document.filename and the new
Document.path, the base later multi-file resolution can use. parse() is
the string form; parse_document() stays as a warning alias.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
…e place, say path is groundwork

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
…nly what was found

- validate() replaces validate_document() (deprecated, removed in 0.5.0).
- The two importer callbacks become one resolve= (same shape as assembly's
  LoadFile); by default the amended original's and the attachment files'
  definitions are read from beside document.path, sandboxed to its directory
  (files.file_loader, now shared with the CLI and assembly).
- ValidationResult drops the stored errors/warnings/infos (now derived from
  diagnostics) and the recording methods and used_terms (internal _Recorder).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
… CLI test, changed-in-0.4.0 note

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
…thing else; CONFORMANCE limit reworded

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
…rts a fault while validating as an internal error

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
…ineValues)

The flat analysis fields leave ValidationResult at once, with no
forwarding: result.sections is result.index.sections, the five inline_*
lists are result.index.values.{dates,money,durations,fields,placeholders}.
The recorder now builds the result from its two parts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
…y keyword

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
…pler removal test

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
…nswers before assembling

Stage 1 of the assembly questions/answers plan.

- load_template/parse_template read a template and its includes once; Template
  has questions, problems (what refuses assembly, with the validator's template
  rules) and validation (advice).
- Template.form(answers) is a frozen snapshot: questions reached (includes read
  where their {{include:}} is), unanswered, blocking, unused, diagnostics,
  ready, complete; problem(qid); assemble(). An invalid answer counts as no
  answer for reach.
- Question is frozen, has problem(answer), and keeps its placeholders' info.
- assemble, template_questions and needed_questions are deprecated (0.4.0,
  removed 0.5.0) and keep their behaviour; legaldown assemble follows the new
  API. The template identifiers are worked out once per template.
- tests/test_assembly.py runs every test through both the functions and Template/Form.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
… twice, CLI guard)

- Question keeps plain dict default/choices (copied from the declaration), so a
  default can be offered back as an answer, and a Question can be copied and
  pickled; None choices is accepted.
- condition-reference-unsafe blocks only a template that includes nothing;
  duration-invalid-unit and directive-duplicate-param block a template.
- A file included twice is read in place at its last include, as the old walk
  decides it. Answers that cannot be copied are reported, not a crash.
- Template takes check=; the form-versus-function tests now compare with the
  deprecated function's ok, output, files and rules; a test pins the reference
  into an include; CLI tests for the refusal and the internal-error guard.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
…-only gate for unsafe references, cyclic answers

- directive-duplicate-param and duration-invalid-unit are checked on placeholders
  outside drafting notes (from the template's own reading), not taken from the
  validator, which also reports them for notes and other directives.
- condition-reference-unsafe is advice only when the template includes fragments;
  an attachment file hides no section from a reference.
- Form copies answers with a memo for cycles, falls back when nested too deep.
- The CLI reports a fault while reading the template as an internal error.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
Stage 2 of the assembly questions/answers plan.

- hint says what to type (yes or no, a date, an amount and a currency, one of:
  the choices, ...); from_text turns typed text into the answer assembly takes,
  or None for the empty text, or raises ValueError saying what to enter.
- Strict, not locale-aware; a key before a label, an ambiguous label refused;
  the amount alone only where the placeholders fix the currency or the unit.
- What it returns always passes Question.problem (checked last, and by a
  randomized test); README documents the interview loop.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
…text errors say why, exact key first, README loop keeps decisions open)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
…wn questions

Stage 3 of the assembly questions/answers plan.

- Template.coerce puts right the shapes plain YAML gets wrong (text read as
  from_text reads it, a money amount given as a number) and never raises;
  load_answers reads the YAML mapping (AnswersError); the CLI uses both.
- Form.as_dict is the JSON-ready form: states, answers, hints, accepts, problems
  and diagnostics with the question each is about. Question.accepts is the
  structured hint, to_text the inverse of from_text.
- legaldown questions lists what is asked (text or JSON, --check).
- The hint and from_text follow the recorded placeholders over a changed currency.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
…d descriptions of hostile answers

- Form.as_dict is strict JSON: non-finite floats and integers too long to write
  become text; nested answers are cut at a depth and a node budget; unknown
  shapes are described shallowly (Question.to_text).
- legaldown questions writes UTF-8 bytes like assemble, allow_nan=False, and
  reports a fault as an internal error; load_answers reports nesting too deep.
- coerce cannot raise for an integer too long to write; as_dict's currency and
  unit follow Question.accepts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
…as empty text

Fixes the as_dict test of the previous commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
Stage 4 of the assembly questions/answers plan.

- -i asks, on a terminal, for the answers --answers does not give, as the form
  grows: prompts and errors on stderr, each answer read with Question.from_text,
  a decision asked again until answered, the empty line leaving a value
  question to its default or blank, an invalid answer from the file asked
  again. Without a terminal it lists what must be answered (exit 1), or
  assembles if nothing must be. The end of the input ends the interview.
- --save-answers FILE writes the answers used, with those typed, as YAML in a
  finally (after Ctrl-C, which assembles nothing and exits 2, or a failure).
- Without -i a missing decision says how to be asked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
…nvalid answers dropped, atomic save, line editing)

- A text answer holding a lone surrogate is not valid; the terminal's input is
  asked again when it cannot be decoded; input() gives line editing, with the
  prompt on stderr.
- A template that cannot be assembled asks nothing (and lists no questions
  without a terminal): its problems come first.
- The empty line drops an answer from the file that is not valid, so the default
  or the blank applies; an accepted default is echoed.
- --save-answers writes beside the file and moves it into place.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
…m of line editing)

input() uses readline only where standard output is the terminal, which assemble's is
not, so the earlier change did not give line editing and let arrow keys into answers.
A text answer with a C0 control (the tab excepted) or DEL is not valid; _ask is the
plain readline() again, with the prompt on stderr.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
The deprecations of this release say 'since 0.4.0, removed in 0.5.0'.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz

dvejsada commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

@claude review


Generated by Claude Code

@claude

claude Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

claude added 3 commits October 2, 2026 16:18
…plain data, one path filter, save-answers keeps what it replaces

- answer_problem is §15.7.1 exactly again (a default with a control character is as valid
  as before); the control-character and UTF-8 rules apply to what is typed (from_text).
- Question's placeholder record is an InitVar, not a field: asdict/replace/fields are plain.
- files.relative_path: one filter for the paths a LoadFile is asked for (validate and
  assembly): no absolute path, drive letter, backslash, null byte or '..'.
- --save-answers keeps the file's permissions, writes through a symlink, refuses the
  template's own path; an answer that cannot be written as UTF-8 is an error, not a traceback;
  the end of the input is said.
- README: validate now reads amended originals and attachments (changed in 0.4.0).
- Tests: spec-exact text answers, the filter, symlinks, save behaviour; the conformance
  tests also run the public Template door.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
…de the directory

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
…f invalid templates documented; hard-link guard

- --save-answers writes nothing when no answers file was read and nothing was typed (a typo in
  the template name, -i without a terminal, Ctrl-C before the first answer), and says so.
- The template is also protected against a hard link named as the file to save to.
- README: 'Changed in 0.4.0' for legaldown assemble refusing templates with template-rule Errors.
- CONFORMANCE.md and the validator __all__ wording.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
claude added 2 commits October 2, 2026 16:43
…, not a traceback

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
…, and replaced by the answers from 3.13

resolve() no longer raises on a link loop in 3.13, so the save succeeds there.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WmBAc5T7UCKVdpUmg9qxz
@dvejsada
dvejsada merged commit 864967c into main Oct 2, 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