M1: block model, readers, profiles, semantic pass, sample pair, Pyodide CI - #129
Merged
Merged
Conversation
Formatting only. Sixteen files had drifted from black's defaults; this brings them back so that black --check is clean before the M1 code lands and feature diffs stay readable. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QosmZFLHNUw7kSDgG3ejXT
Lands the foundations every M1 reader builds on: - redlines/blocks.py: the frozen block model (#98) with role and spans (#99), matched_by, confidence and dropped reporting with a derived fallback_count (#106); XPath-style addresses assigned by one function, a heading breadcrumb, and to_dict/from_dict round-tripping. - redlines/readers: the runtime-checkable Reader protocol and registry (#105), a placeholder ParagraphReader as the degrade path, and a shared input size cap; redlines/readers/detect.py detects txt and md by extension then content, and reports unknown types instead of guessing (#107). - examples/custom_reader.py: a worked third-party reader, documented in examples/README.md and executed by the test suite. - ADR-0029 (address syntax) and ADR-0030 (matched_by and confidence). Nothing is exported from the top-level package yet; Redlines, Document and the CLI are unchanged. Refs #98, #99, #105, #106, #107. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QosmZFLHNUw7kSDgG3ejXT
redlines/readers/labels.py holds what the markdown reader will reuse: label detection against a profile's patterns, a style stack that resolves alpha/roman ambiguity from the numbering run, continuation attachment and heading scoring. redlines/readers/text.py is the PlainTextReader over the five PRD § 6b stages, registered for "text" in place of the ParagraphReader placeholder, and the enforcement point for the ADR-0028 size cap. Every stage records what it decided in attrs. The PRD § 6b hard cases live under tests/corpus/hard_cases/. Four pass: alpha/roman ambiguity at (i), numbering restarts inside schedules, one-line clauses that look like headings, mixed label styles. Three are strict xfails with reasons: run-on definitions, cross-references in prose, PDF page headers (which must not crash, and do not). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QosmZFLHNUw7kSDgG3ejXT
Three commented YAML profiles under redlines/profiles/builtin/, loaded by name through builtin_profile() and validated by the existing loader. generic is paragraphs only; contract covers decimal, alpha, roman and word labels, schedule resets, the definitions, recital, schedule and signature roles and the defined_term, cross_reference, party, date and amount spans; markdown carries the same rules for what survives syntax stripping, since headings come from #. The markdown profile repeats most of contract's span extractors and role rules, which is the evidence ADR-0028 asked #101 to look for before adding a composition mechanism. Pinned in a test; the decision is left open. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QosmZFLHNUw7kSDgG3ejXT
A new pyodide job in python-package.yml builds the wheel, loads it into Pyodide 0.28.3 under Node 22 through micropip, and imports redlines and every M1 subpackage. The first run of that check found a real bug: the package imports typing_extensions without declaring it, so a clean install on Python 3.10 or in the browser fails. Unpack now comes from typing on 3.11+ and typing-extensions is declared for older Pythons. Also folds in four small wave A follow-ups: the custom_reader example output in examples/README.md, its hardcoded file name, and two detect_format edge cases (OLE reason wording, magic bytes in str input). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QosmZFLHNUw7kSDgG3ejXT
The reader and the profiles were built independently, and meeting them exposed three defects the isolated tests could not see: - The built-in roman_paren pattern required two letters, so a bare "(i)" never reached the style stack and the PRD's canonical case ((i) after 7.2 is roman, one level deeper) came out alpha. The pattern now admits single letters and the stack's ranking decides, as designed. - Hard-wrap re-joining had dropped the PRD § 6b condition that the next line begins lowercase, so "2. Charges" swallowed the body line under it. The rule is restored; a capitalised resumption still joins when the previous line is long enough to be a plausible wrap and is not heading-shaped. - With both an alpha and a roman run open, a value continuing neither exactly fell to profile order, so "(x)" after "(v)" popped out of the roman sub-list. The open-style rank now prefers the smaller forward jump within its run. tests/test_reader_profiles_integration.py runs the text reader with every built-in profile over the hard cases so this seam stays covered. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QosmZFLHNUw7kSDgG3ejXT
redlines/readers/markdown.py reads ATX and setext headings, ordered and unordered lists nested by indentation, pipe tables, fenced code and blockquotes with stdlib re, then hands list-item and paragraph text to the same label detection, hierarchy stack, continuation and heading scoring the plain-text reader uses. The tree shape is the text reader's, so a markdown contract and its plain-text twin produce the same kinds, labels, levels and addresses; a twin fixture proves it block by block. Horizontal rules, HTML blocks, link reference definitions and images are dropped and reported. Quoted material is read but never structures the document. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QosmZFLHNUw7kSDgG3ejXT
redlines/semantic.py adds apply_semantics(tree, profile), a pure pass that assigns roles from the profile's role rules (first match wins; ancestor_heading resolves nearest-first per ADR-0028) and spans from its extractors (all run, every non-overlapping match kept). A cross_reference span carries the referenced label as its value so M2 can report a reference that followed a renumbering. The PRD § 6b definitions heuristic (quoted term, "means", text) is the one rule the profile format cannot express and is written in Python behind the definition role, recording its decision in attrs. Block now validates that every span lies within its text. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QosmZFLHNUw7kSDgG3ejXT
tests/corpus/sample_pair/ holds the demo agreement from PRD § 3a: a forty-odd clause services agreement between two fictional parties, in markdown and as a plain-text twin, and an amended version carrying exactly one of each change the engine is meant to detect: a changed definition, a clause moved from section 7 to section 9 with an edit in its body, a renumbering from an inserted clause, a cross-reference that follows it, a deleted sub-clause, an inserted table row, a whitespace- only non-change, and an edit inside a repetitive schedule. CHANGES.md lists each with its addresses. expected/ freezes the four trees (text under contract, markdown under markdown, through the semantic pass); regenerate.py rewrites them and tests/test_sample_pair.py rebuilds each tree and asserts equality plus the structural facts the pair exists to show, including that the text and markdown twins agree block by block. The change-tree golden is M2's. The hand-built twin trees in tests/test_semantic.py are replaced by the real readers over the merged twin fixture; one of them had encoded a wrong level. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QosmZFLHNUw7kSDgG3ejXT
Two follow-ups from the M1 run, neither new milestone scope. redlines/profiles/builtin.py moves into redlines/profiles/builtin/, the directory holding the three YAML files it loads. The module and the data directory previously shared a name; import resolution happened to prefer the module, but the collision was confusing to read and fragile under namespace-package tooling. Making the directory a real package removes the ambiguity without changing a single import spelling, and the wheel listing is unchanged apart from the moved file. redlines/pipeline.py adds read_document(), which composes the three M1 stages a caller otherwise has to spell out: pick a profile, read with the reader for the format, apply the semantic pass. Format is detected when not given, and an undetectable one raises with the detection reason quoted, so the "coming in 1.1" message reaches the caller. A profile can be a built-in name, a Profile, a mapping or a file; a built-in name is resolved before any path, so "contract" can never be shadowed by a file of that name. With no profile it applies the default the roadmap already recorded: contract for text, markdown for markdown. The sample-pair regenerate script now calls it, and the goldens are byte-identical afterwards. tests/test_sample_pair.py keeps spelling the pipeline out by hand on purpose: that duplication is what makes a drift between the two a test failure rather than a silently regenerated golden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QosmZFLHNUw7kSDgG3ejXT
Adds the completion entry M0 already has: the evidence for each of the three exit criteria, the two ADRs the milestone was asked to write, and the bug the Pyodide check found on its first run. Also records the two decisions M1 surfaced and did not take. The profile format cannot assign a clause role, because all three role match kinds are structural, so 72 of the 102 blocks in the sample pair carry none; that is now #130, to be decided before M2 has change nodes carry roles. ADR-0028's composition revisit condition is met and left open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QosmZFLHNUw7kSDgG3ejXT
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this lands
The M1 milestone in full: the block model, both 1.0 readers, the built-in profiles, the semantic pass, the section 3a sample pair with its expected trees, and the Pyodide CI check. Nothing is exported from the top-level package yet;
Redlines,Documentand the CLI are unchanged apart from one import line.Wave A: foundations
redlines/blocks.py— the frozen block model (Block model dataclasses: kind, text, label, level, path, children, attrs #98) withroleandspans(Semantic fields: role on blocks, spans in blocks #99);matched_by,confidence,droppedreporting and a derivedfallback_count(dropped reporting, per-block matched_by and confidence, tree-level fallback count #106); XPath-style addresses assigned by one function, a heading breadcrumb, andto_dict/from_dictround-tripping.redlines/readers/— the runtime-checkableReaderprotocol and registry (Reader interface with a worked third-party example #105), aParagraphReaderas the degrade path, a shared input size cap, anddetect.pyfor txt and md detection that reports unknown types instead of guessing (Format detection for txt and md; unknown types reported, not guessed #107).examples/custom_reader.py— the worked third-party reader, executed by the test suite.matched_byand confidence semantics).Wave B1: text reader, built-in profiles, Pyodide CI
redlines/readers/labels.pyandtext.py— the plain-text reader over PRD § 6b's five stages (Plain-text reader: normalise, segment, re-join wraps, detect labels, infer hierarchy #102). The § 6b hard cases live undertests/corpus/hard_cases/: four pass (alpha/roman at(i), numbering restarts inside schedules, one-line clauses that look like headings, mixed label styles) and three are strict xfails with reasons (run-on definitions, cross-references in prose, PDF page headers, which must not crash and do not).redlines/profiles/builtin/—generic,contractandmarkdownas commented YAML (Built-in profiles: generic, contract, markdown #101). The markdown profile repeats most of contract's rules; that is the evidence ADR-0028 asked Built-in profiles: generic, contract, markdown #101 to gather before deciding on composition, and the decision is left open.python-package.yml(Pyodide import check in CI #109). Its first run found a real bug: an undeclaredtyping_extensionsimport that broke clean installs on Python 3.10 and in the browser. Fixed here.(i)now reaches the style stack, hard-wrap re-joining follows the PRD's lowercase rule, and the stack prefers the smaller forward jump when two runs are open.Wave B2: markdown reader, semantic pass
redlines/readers/markdown.py— stdlib-regex reader over the same label and hierarchy logic (Markdown reader: headings, nested lists, clause patterns, pipe tables, fenced code #103). A markdown contract and its plain-text twin produce the same kinds, labels, levels and addresses;tests/corpus/markdown_cases/twin_contract.*proves it block by block.redlines/semantic.py—apply_semantics(tree, profile), pure and idempotent (Semantic pass: definitions, schedules, cross-references, parties, dates, amounts #104): roles first-match-wins with ancestor proximity as ADR-0028's one exception, spans from every extractor, cross-references carrying the referenced label as the span's value, and the PRD § 6b definitions heuristic as the one rule the profile format cannot express.Wave C: the sample pair
tests/corpus/sample_pair/— the PRD § 3a demo agreement (Section 3a sample pair and its expected block trees #108): a services agreement between Northwind Systems Limited and Harbour Foods Group Limited, in markdown and as a plain-text twin, plus an amended version carrying exactly one of each detectable change.CHANGES.mdlists the eight changes with their addresses.expected/freezes the four trees;tests/test_sample_pair.pyrebuilds and compares them and asserts the structural facts the pair exists to show. The change-tree golden is M2's.Follow-ups
redlines/pipeline.py—read_document()composes the three stages a caller otherwise spells out by hand. Format is detected when not given, and an undetectable one raises with the detection reason quoted so the "coming in 1.1" message reaches the caller. A profile can be a built-in name, aProfile, a mapping or a file, with a built-in name resolved before any path so"contract"cannot be shadowed by a file of that name. With no profile it applies the default the roadmap already recorded:contractfor text,markdownfor markdown. The sample-pair regenerate script now calls it and the goldens are byte-identical afterwards.redlines/profiles/builtin/is now a package, holding both the loader and the three YAML files it reads. The module and the data directory previously shared a name. No import spelling changed and the wheel listing is unchanged apart from the moved file.M1 exit criteria
contractandmarkdown: 53 tests intest_sample_pair.py.pyodidejob is green on every push.Checks
Decisions
Settled: the sample agreement text stands as drafted, and the
clauserole question is now tracked as #130 rather than blocking this PR.Still open, neither blocking:
recitalandsignaturearematch: headingin the contract profile, so the blocks under those headings carry no role;scheduleusesancestor_headingand reaches the whole subtree. One-line profile change if you want them to match.Worth knowing rather than deciding: the four expected trees total about 480 KB of pretty-printed JSON, and the plain-text twin states Schedule 1's deliverables as sentences rather than a table, because the text reader has no tables (ADR-0013). The twins therefore diverge in exactly that subtree, and the test says so.
Closes #98, closes #99, closes #101, closes #102, closes #103, closes #104, closes #105, closes #106, closes #107, closes #108, closes #109.
🤖 Generated with Claude Code
https://claude.ai/code/session_01QosmZFLHNUw7kSDgG3ejXT