lexichunk 0.9.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.
Files changed (143) hide show
  1. lexichunk-0.9.0/CHANGELOG.md +410 -0
  2. lexichunk-0.9.0/CONTRIBUTING.md +75 -0
  3. lexichunk-0.9.0/LICENSE +21 -0
  4. lexichunk-0.9.0/MANIFEST.in +5 -0
  5. lexichunk-0.9.0/PKG-INFO +881 -0
  6. lexichunk-0.9.0/README.md +831 -0
  7. lexichunk-0.9.0/SECURITY.md +69 -0
  8. lexichunk-0.9.0/benchmarks/conftest.py +29 -0
  9. lexichunk-0.9.0/benchmarks/test_perf_chunk.py +81 -0
  10. lexichunk-0.9.0/docs/adoption-guide.md +114 -0
  11. lexichunk-0.9.0/docs/architecture.md +333 -0
  12. lexichunk-0.9.0/docs/extending.md +170 -0
  13. lexichunk-0.9.0/docs/ingestion.md +236 -0
  14. lexichunk-0.9.0/examples/basic_usage.py +208 -0
  15. lexichunk-0.9.0/examples/compare_chunkers.py +407 -0
  16. lexichunk-0.9.0/examples/docling_pipeline.py +216 -0
  17. lexichunk-0.9.0/examples/langchain_rag.py +233 -0
  18. lexichunk-0.9.0/examples/llm_fallback.py +164 -0
  19. lexichunk-0.9.0/examples/offline_evidence_retrieval.py +105 -0
  20. lexichunk-0.9.0/pyproject.toml +111 -0
  21. lexichunk-0.9.0/setup.cfg +4 -0
  22. lexichunk-0.9.0/src/lexichunk/__init__.py +66 -0
  23. lexichunk-0.9.0/src/lexichunk/_patterns.py +66 -0
  24. lexichunk-0.9.0/src/lexichunk/chunker.py +2049 -0
  25. lexichunk-0.9.0/src/lexichunk/documents.py +571 -0
  26. lexichunk-0.9.0/src/lexichunk/enrichment/__init__.py +1 -0
  27. lexichunk-0.9.0/src/lexichunk/enrichment/clause_type.py +660 -0
  28. lexichunk-0.9.0/src/lexichunk/enrichment/context.py +152 -0
  29. lexichunk-0.9.0/src/lexichunk/exceptions.py +25 -0
  30. lexichunk-0.9.0/src/lexichunk/ingestion/__init__.py +49 -0
  31. lexichunk-0.9.0/src/lexichunk/ingestion/_common.py +240 -0
  32. lexichunk-0.9.0/src/lexichunk/ingestion/docling.py +258 -0
  33. lexichunk-0.9.0/src/lexichunk/ingestion/markdown.py +111 -0
  34. lexichunk-0.9.0/src/lexichunk/ingestion/unstructured.py +171 -0
  35. lexichunk-0.9.0/src/lexichunk/integrations/__init__.py +1 -0
  36. lexichunk-0.9.0/src/lexichunk/integrations/langchain.py +361 -0
  37. lexichunk-0.9.0/src/lexichunk/integrations/llama_index.py +440 -0
  38. lexichunk-0.9.0/src/lexichunk/jurisdiction/__init__.py +207 -0
  39. lexichunk-0.9.0/src/lexichunk/jurisdiction/eu.py +182 -0
  40. lexichunk-0.9.0/src/lexichunk/jurisdiction/uk.py +144 -0
  41. lexichunk-0.9.0/src/lexichunk/jurisdiction/us.py +259 -0
  42. lexichunk-0.9.0/src/lexichunk/metrics.py +106 -0
  43. lexichunk-0.9.0/src/lexichunk/models.py +661 -0
  44. lexichunk-0.9.0/src/lexichunk/offsets.py +369 -0
  45. lexichunk-0.9.0/src/lexichunk/parsers/__init__.py +15 -0
  46. lexichunk-0.9.0/src/lexichunk/parsers/definitions.py +831 -0
  47. lexichunk-0.9.0/src/lexichunk/parsers/references.py +954 -0
  48. lexichunk-0.9.0/src/lexichunk/parsers/structure.py +1352 -0
  49. lexichunk-0.9.0/src/lexichunk/py.typed +0 -0
  50. lexichunk-0.9.0/src/lexichunk/strategies/__init__.py +34 -0
  51. lexichunk-0.9.0/src/lexichunk/strategies/_cascade.py +186 -0
  52. lexichunk-0.9.0/src/lexichunk/strategies/clause_aware.py +923 -0
  53. lexichunk-0.9.0/src/lexichunk/strategies/fallback.py +448 -0
  54. lexichunk-0.9.0/src/lexichunk/utils.py +92 -0
  55. lexichunk-0.9.0/src/lexichunk.egg-info/PKG-INFO +881 -0
  56. lexichunk-0.9.0/src/lexichunk.egg-info/SOURCES.txt +141 -0
  57. lexichunk-0.9.0/src/lexichunk.egg-info/dependency_links.txt +1 -0
  58. lexichunk-0.9.0/src/lexichunk.egg-info/requires.txt +27 -0
  59. lexichunk-0.9.0/src/lexichunk.egg-info/top_level.txt +1 -0
  60. lexichunk-0.9.0/tests/__init__.py +0 -0
  61. lexichunk-0.9.0/tests/_snapshot_support.py +106 -0
  62. lexichunk-0.9.0/tests/conftest.py +134 -0
  63. lexichunk-0.9.0/tests/fixtures/eu_gdpr_excerpt.txt +75 -0
  64. lexichunk-0.9.0/tests/fixtures/generators/_emit.py +643 -0
  65. lexichunk-0.9.0/tests/fixtures/generators/generate_uk_pdf_extracted_agreement.py +1259 -0
  66. lexichunk-0.9.0/tests/fixtures/generators/generate_us_msa_signed_with_exhibits.py +1056 -0
  67. lexichunk-0.9.0/tests/fixtures/gold/uk_pdf_extracted_agreement.json +1346 -0
  68. lexichunk-0.9.0/tests/fixtures/gold/us_msa_signed_with_exhibits.json +934 -0
  69. lexichunk-0.9.0/tests/fixtures/uk_pdf_extracted_agreement.txt +664 -0
  70. lexichunk-0.9.0/tests/fixtures/uk_service_agreement.txt +484 -0
  71. lexichunk-0.9.0/tests/fixtures/uk_terms_conditions.txt +316 -0
  72. lexichunk-0.9.0/tests/fixtures/us_msa.txt +422 -0
  73. lexichunk-0.9.0/tests/fixtures/us_msa_signed_with_exhibits.txt +523 -0
  74. lexichunk-0.9.0/tests/fixtures/us_terms_of_service.txt +420 -0
  75. lexichunk-0.9.0/tests/snapshots/eu_gdpr_excerpt.json +240 -0
  76. lexichunk-0.9.0/tests/snapshots/eu_gdpr_excerpt.summary.json +7 -0
  77. lexichunk-0.9.0/tests/snapshots/uk_pdf_extracted_agreement.json +1431 -0
  78. lexichunk-0.9.0/tests/snapshots/uk_pdf_extracted_agreement.summary.json +7 -0
  79. lexichunk-0.9.0/tests/snapshots/uk_service_agreement.json +1271 -0
  80. lexichunk-0.9.0/tests/snapshots/uk_service_agreement.summary.json +7 -0
  81. lexichunk-0.9.0/tests/snapshots/uk_terms_conditions.json +813 -0
  82. lexichunk-0.9.0/tests/snapshots/uk_terms_conditions.summary.json +7 -0
  83. lexichunk-0.9.0/tests/snapshots/us_msa.json +1504 -0
  84. lexichunk-0.9.0/tests/snapshots/us_msa.summary.json +7 -0
  85. lexichunk-0.9.0/tests/snapshots/us_msa_signed_with_exhibits.json +1366 -0
  86. lexichunk-0.9.0/tests/snapshots/us_msa_signed_with_exhibits.summary.json +7 -0
  87. lexichunk-0.9.0/tests/snapshots/us_terms_of_service.json +1471 -0
  88. lexichunk-0.9.0/tests/snapshots/us_terms_of_service.summary.json +7 -0
  89. lexichunk-0.9.0/tests/test_abbreviations.py +238 -0
  90. lexichunk-0.9.0/tests/test_adversarial_extensibility.py +590 -0
  91. lexichunk-0.9.0/tests/test_adversarial_v050.py +459 -0
  92. lexichunk-0.9.0/tests/test_adversarial_v060.py +318 -0
  93. lexichunk-0.9.0/tests/test_adversarial_v070.py +281 -0
  94. lexichunk-0.9.0/tests/test_adversarial_v080.py +262 -0
  95. lexichunk-0.9.0/tests/test_ancestor_headers.py +127 -0
  96. lexichunk-0.9.0/tests/test_batch.py +177 -0
  97. lexichunk-0.9.0/tests/test_char_offsets.py +268 -0
  98. lexichunk-0.9.0/tests/test_chunk_documents.py +485 -0
  99. lexichunk-0.9.0/tests/test_chunk_iter.py +47 -0
  100. lexichunk-0.9.0/tests/test_chunker.py +981 -0
  101. lexichunk-0.9.0/tests/test_classification_confidence.py +151 -0
  102. lexichunk-0.9.0/tests/test_classification_hook.py +388 -0
  103. lexichunk-0.9.0/tests/test_clause_types.py +330 -0
  104. lexichunk-0.9.0/tests/test_context_enricher.py +123 -0
  105. lexichunk-0.9.0/tests/test_crossref_stats.py +136 -0
  106. lexichunk-0.9.0/tests/test_definition_cache.py +96 -0
  107. lexichunk-0.9.0/tests/test_definitions.py +429 -0
  108. lexichunk-0.9.0/tests/test_definitions_performance.py +147 -0
  109. lexichunk-0.9.0/tests/test_docstrings.py +113 -0
  110. lexichunk-0.9.0/tests/test_eu_jurisdiction.py +358 -0
  111. lexichunk-0.9.0/tests/test_exceptions.py +161 -0
  112. lexichunk-0.9.0/tests/test_exhibit_letter_references.py +122 -0
  113. lexichunk-0.9.0/tests/test_extra_clause_signals.py +104 -0
  114. lexichunk-0.9.0/tests/test_g1_structure.py +586 -0
  115. lexichunk-0.9.0/tests/test_g2_strategies.py +440 -0
  116. lexichunk-0.9.0/tests/test_g3_parsers.py +679 -0
  117. lexichunk-0.9.0/tests/test_g4a_api.py +813 -0
  118. lexichunk-0.9.0/tests/test_g4b_integrations.py +581 -0
  119. lexichunk-0.9.0/tests/test_g6_polish.py +521 -0
  120. lexichunk-0.9.0/tests/test_g7_adversarial_fixes.py +1231 -0
  121. lexichunk-0.9.0/tests/test_gold_fixtures.py +566 -0
  122. lexichunk-0.9.0/tests/test_heading_regression.py +233 -0
  123. lexichunk-0.9.0/tests/test_heading_shapes.py +211 -0
  124. lexichunk-0.9.0/tests/test_ingestion.py +606 -0
  125. lexichunk-0.9.0/tests/test_integrations.py +482 -0
  126. lexichunk-0.9.0/tests/test_invariants.py +382 -0
  127. lexichunk-0.9.0/tests/test_jurisdiction_registry.py +182 -0
  128. lexichunk-0.9.0/tests/test_metrics.py +285 -0
  129. lexichunk-0.9.0/tests/test_offline_example.py +13 -0
  130. lexichunk-0.9.0/tests/test_offset_map.py +367 -0
  131. lexichunk-0.9.0/tests/test_position_scoring.py +134 -0
  132. lexichunk-0.9.0/tests/test_properties.py +145 -0
  133. lexichunk-0.9.0/tests/test_random_invariants.py +356 -0
  134. lexichunk-0.9.0/tests/test_readme_examples.py +180 -0
  135. lexichunk-0.9.0/tests/test_redos_audit.py +239 -0
  136. lexichunk-0.9.0/tests/test_references.py +378 -0
  137. lexichunk-0.9.0/tests/test_release_workflows.py +91 -0
  138. lexichunk-0.9.0/tests/test_sanitization.py +154 -0
  139. lexichunk-0.9.0/tests/test_serialization.py +221 -0
  140. lexichunk-0.9.0/tests/test_snapshots.py +101 -0
  141. lexichunk-0.9.0/tests/test_structure_metrics.py +255 -0
  142. lexichunk-0.9.0/tests/test_structure_parser.py +373 -0
  143. lexichunk-0.9.0/tests/test_tc_specialisation.py +232 -0
@@ -0,0 +1,410 @@
1
+ # Changelog
2
+
3
+ All notable changes to lexichunk are documented in this file. The format is
4
+ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
+
6
+ ## [0.9.0] — 2026-09-06
7
+
8
+ First tagged release of the source-distributed public beta. lexichunk is not
9
+ published on PyPI; install it from git, pinned to `v0.9.0` or to a reviewed
10
+ commit SHA. Everything before this version was source-only too, so the
11
+ "Breaking" entries below describe changes against `0.8.0b1` as installed from
12
+ git, not against a published package.
13
+
14
+ ### Breaking
15
+ - `chunk_batch()` rejects a bare `str`, `bytes` or `bytearray` passed as
16
+ `texts`, raising `InputError` instead of silently iterating the string
17
+ character-by-character. Any other iterable — list, tuple, generator,
18
+ `dict.values()` — is still accepted. Pass `chunk_batch([text])` for a
19
+ single document.
20
+ - `chunk_batch()` also rejects a `Mapping`. Iterating one yields its *keys*,
21
+ so `chunk_batch({"doc1": text1})` used to chunk the string `"doc1"` and
22
+ never look at the document, returning a normal-looking `BatchResult` with
23
+ `errors == []`. Pass `mapping.values()`, or `mapping.items()` to use the
24
+ keys as document ids.
25
+ - `ClauseType`, `Jurisdiction` and `DocumentSection` are now string-valued
26
+ (`str` mixin) rather than plain `Enum` members. Equality against their
27
+ string form (`chunk.clause_type == "indemnification"`) now holds, and
28
+ `json.dumps` accepts them directly — but code relying on `repr()` output
29
+ or on `type(x) is Enum` identity checks should be reviewed.
30
+ - `register_jurisdiction()` raises `ConfigurationError` by default when
31
+ re-registering an existing jurisdiction key; pass `override=True` to
32
+ replace an existing registration explicitly. Previously, re-registration
33
+ silently replaced the existing entry.
34
+ - `LegalTextSplitter.split_text()` returns LangChain `Document` objects
35
+ rather than `str`. This differs from the base `TextSplitter.split_text()`
36
+ signature and is deliberate — the metadata is the point of the library —
37
+ and is now documented as such in the README rather than left implicit.
38
+ - The maximum input size (`_MAX_INPUT_CHARS`, 10,000,000 characters) is now
39
+ checked against the *raw* input, before sanitisation. Input that used to
40
+ slip past the guard because it sanitised down below the limit (for
41
+ example a multi-megabyte run of BOM characters) now raises `InputError`.
42
+
43
+ ### Added
44
+ - `LegalChunk.to_dict()` / `LegalChunk.from_dict()` for JSON-safe
45
+ round-tripping of chunk data (caching, cross-process transfer). `to_dict()`
46
+ is also defined on `HierarchyNode`, `CrossReference` and `DefinedTerm`.
47
+ - `CrossReference.target_kind` — the kind of thing a reference points at
48
+ (`"clause"`, `"section"`, `"paragraph"`, `"schedule"`, `"exhibit"`,
49
+ `"annex"`, `"chapter"`, `"recital"`, …), so a `Schedule 2` reference is no
50
+ longer confused with a main-body `clause 2`. Also emitted in the
51
+ integrations' flattened cross-reference metadata.
52
+ - `tests/snapshots/*.json` golden-file snapshots for every fixture and a
53
+ `pytest --update-snapshots` flag for regenerating them (see
54
+ `CONTRIBUTING.md` — review the diff before committing).
55
+ - `tests/test_invariants.py` — cross-cutting, Hypothesis-backed invariant
56
+ tests that run independently of any single stage's unit tests.
57
+ - `tests/test_readme_examples.py` executes every Python block in `README.md`,
58
+ so documented code cannot drift from the API; `tests/test_docstrings.py`
59
+ requires a docstring on every public export and pins the `LegalChunker`
60
+ public surface; `tests/test_heading_regression.py` is a flat, table-driven
61
+ set of 25 realistic headings that must be detected and the heading-shaped
62
+ lines (postal addresses, currency amounts, dates, durations,
63
+ table-of-contents entries) that must not be; and
64
+ `tests/test_heading_shapes.py` pins the per-line `detect_level` shapes
65
+ underneath that gate, US bare-decimal headings in particular.
66
+ - Two derandomised Hypothesis profiles in `tests/conftest.py` (`dev` and
67
+ `ci`), so a property-based failure reproduces from the same commit on any
68
+ machine. CI selects `ci` via `HYPOTHESIS_PROFILE`.
69
+ - `LegalTextSplitter.split_documents()` and `.transform_documents()` for
70
+ chunking already-loaded LangChain `Document` objects, preserving caller
71
+ metadata (lexichunk's own keys win on collision); `create_documents()` now
72
+ accepts a parallel `metadatas=` list, and `LegalTextSplitter` gained
73
+ `include_defined_terms_context`, `flatten_metadata` and `metadata_prefix`
74
+ options.
75
+ - `LegalNodeParser` now subclasses LlamaIndex's `NodeParser`, so nodes carry
76
+ `relationships` (`SOURCE`/`PREVIOUS`/`NEXT`) and `ref_doc_id`; structural
77
+ metadata keys are excluded from embedding and LLM text via
78
+ `excluded_embed_metadata_keys` by default.
79
+ - `register_jurisdiction(..., override=True)`, `unregister_jurisdiction()`
80
+ and `registered_jurisdictions()` for inspecting and managing the
81
+ jurisdiction registry at runtime.
82
+ - `LegalChunker.sanitize(text)` — a static method exposing the same BOM
83
+ stripping, CRLF→LF and Unicode NFC normalisation the pipeline applies
84
+ internally, so callers can slice the exact string `char_start`/`char_end`
85
+ index into.
86
+ - `LegalChunker.jurisdiction` property for reading back the configured
87
+ jurisdiction.
88
+ - A heading-plausibility gate in structure parsing, so lines that only
89
+ superficially resemble a heading (postal addresses, currency amounts,
90
+ dates, list items) no longer open a new clause.
91
+ - Per-jurisdiction section roles and roman/alpha identifier disambiguation
92
+ in the structure parser, plus header-coverage accounting so body text is
93
+ not silently dropped between detected headers.
94
+ - Hierarchy-aware merge: undersized clauses merge only with an adjacent
95
+ sibling under the same parent — the hierarchy is never crossed to satisfy
96
+ `min_chunk_size`.
97
+ - `max_chunk_size` is enforced as a hard cap via a cascading splitter
98
+ (sentence → semicolon → enumerator → newline → word window → character
99
+ window), shared by the clause-aware and fallback paths, with a single
100
+ `WARNING` logged when a run offers no boundary inside the budget and the
101
+ cut therefore lands mid-word.
102
+ - `chunk_batch()` falls back to serial execution (with a `WARNING` log) when
103
+ the process pool cannot be started, instead of raising; a generator that
104
+ raises partway through is recorded as one `BatchError` at the index it
105
+ stopped on rather than propagating out of the call.
106
+ - Four commercial `ClauseType` members — `SERVICES`, `INSURANCE`, `AUDIT`
107
+ and `NON_SOLICITATION` — bringing the classifier to 31 clause types.
108
+ `secondary_clause_type` semantics are unchanged.
109
+ - `classification_confidence` is now saturation-scaled:
110
+ `(best_score / total_score) * min(1.0, best_score / 4.0)`, so a clause
111
+ whose winning type has thin absolute evidence no longer reports high
112
+ confidence just because nothing else scored.
113
+ - The definition cache is a thread-safe LRU (`OrderedDict` under a
114
+ `threading.Lock`, least-recently-used eviction) rather than an unguarded
115
+ FIFO dict.
116
+ - Container headings written over two lines (`ARTICLE I` above `DEFINITIONS`,
117
+ `Chapter I` above `General provisions`) adopt the second line as the clause
118
+ title, so `hierarchy_path` reads `Article I — Definitions`. The line stays
119
+ in the body text and no offsets change.
120
+ - Descendants of a Schedule / Exhibit / Annex (or of a Recitals or
121
+ Definitions block) inherit that container's `DocumentSection`, so
122
+ `SCHEDULE 1 > 1 — Overview` is `SCHEDULES` and `1 — Definitions > 1.1` is
123
+ `DEFINITIONS` rather than `OPERATIVE`. A descendant with a section of its
124
+ own keeps it. This also lets cross-reference resolution tell a main-body
125
+ `clause 3` from a Schedule's paragraph 3.
126
+ - Chunks whose entire body was a heading line (`Article I`, `Chapter I`,
127
+ `SCHEDULE 1 — SERVICES DESCRIPTION`) are folded into the child clause they
128
+ announce, as long as the result fits `max_chunk_size`. The absorbed
129
+ heading's identifier is recorded, so `Schedule 1` / `Article I` references
130
+ still resolve to the merged chunk.
131
+ - A term redefined for a schedule ("For the purposes of this Schedule 2
132
+ only, 'Services' means …") is now scoped to that container: chunks inside
133
+ the schedule get the local definition, main-body chunks keep the
134
+ document-wide one.
135
+ - EU pinpoint references (`Article 6(1)(a)`), reference ranges, and a
136
+ prefix index for faster cross-reference resolution.
137
+ - Packaging/CI: Python 3.13 classifier and CI matrix entry, `windows-latest`
138
+ CI coverage (Python 3.12), an `examples` extra
139
+ (`langchain-text-splitters`, `langchain-community`, `langchain-openai`),
140
+ upper version bounds on `langchain-core` and `llama-index-core`,
141
+ `MANIFEST.in`, a dedicated `integrations.yml` workflow that builds and
142
+ smoke-tests the wheel and sdist, `dependabot.yml`, `SECURITY.md`,
143
+ `CONTRIBUTING.md`, `CODEOWNERS`, issue and pull-request templates, and a
144
+ `--cov-fail-under=92` coverage gate.
145
+ - Installation guidance for a package that is not on PyPI: pin the `v0.9.0`
146
+ tag or an exact reviewed commit SHA. Security reporting scope,
147
+ optional-dependency scope (including the NLTK advisory that reaches the
148
+ `llama-index` extra transitively), an adoption guide
149
+ (`docs/adoption-guide.md`) and a fully offline source-evidence retrieval
150
+ example (`examples/offline_evidence_retrieval.py`, covered by
151
+ `tests/test_offline_example.py`) are documented.
152
+ - Release CI preserves the protected Linux check contexts `test (3.10)`,
153
+ `test (3.11)` and `test (3.12)` by running Windows as its own job rather
154
+ than as an extra matrix axis, and the publish workflow now releases only
155
+ the wheel and source distribution already built and verified by the
156
+ integrations workflow, downloaded as an artifact instead of rebuilt.
157
+ `tests/test_release_workflows.py` compiles the version-check job's embedded
158
+ Python so a broken heredoc fails CI rather than the release.
159
+
160
+ - `LegalChunker(include_ancestor_headers=...)`. Controls what `chunk.content`
161
+ holds. Default `True` keeps the previous behaviour — the chunk's span with
162
+ its ancestor headings prepended. With `False`, `content` is exactly
163
+ `sanitized_text[char_start:char_end]` and `original_header` is empty, which
164
+ is what you want when the offsets drive highlighting or answer-span
165
+ mapping. Chunk boundaries are identical either way.
166
+ - `PipelineMetrics.chunks_unclassified` — chunks whose `clause_type` is
167
+ `UNKNOWN`. A few are normal; `chunks_unclassified == chunk_count` means the
168
+ document carries no clause metadata at all.
169
+ - `LegalChunker.chunk_documents()` — runs the pipeline on structure supplied
170
+ by an external parser (Docling, `unstructured`, a DOCX outline), skipping
171
+ lexichunk's own line-based heading detection.
172
+ - `lexichunk.ingestion` — `from_docling`, `from_unstructured`,
173
+ `from_markdown`. Adapters onto `chunk_documents()`. No new mandatory
174
+ dependencies; importing the package never imports `docling_core` or
175
+ `unstructured`.
176
+ - Raw-offset back-map: `LegalChunker.sanitize_with_map()`, the `OffsetMap`
177
+ type, and `chunk(..., raw_offsets=True)`, which populates `raw_char_start` /
178
+ `raw_char_end` on every chunk so offsets can be mapped back to the text you
179
+ passed in rather than the sanitised text.
180
+ - Structure-quality metrics on `PipelineMetrics`: `clause_count`,
181
+ `top_level_clause_count`, `chunks_spanning_multiple_top_level_clauses`,
182
+ `chunks_with_multiple_clauses`, `chunks_below_min`,
183
+ `heading_candidates_rejected`.
184
+ - `classification_hook` / `classification_hook_threshold` — call your own
185
+ classifier only for chunks the keyword scorer was unsure about.
186
+ - Two gold-annotated fixtures with parser-independent answer keys:
187
+ `uk_pdf_extracted_agreement` (a UK agreement as a naive PDF text extractor
188
+ leaves it — running headers and footers, 78-column hard wrapping,
189
+ cross-references split across line breaks, a soft-hyphenated word) and
190
+ `us_msa_signed_with_exhibits` (a signed US MSA with two-line ARTICLE titles
191
+ and exhibits after the signature block), plus `docs/ingestion.md` and
192
+ `tests/test_gold_fixtures.py`.
193
+
194
+ ### Fixed
195
+ - `DefinitionsExtractor` now uses the jurisdiction registry's `detect_level`
196
+ for clause-boundary detection instead of three hardcoded UK/US/EU regexes.
197
+ A jurisdiction registered through the public `register_jurisdiction()` API
198
+ previously had its term patterns honoured but its clause boundaries
199
+ silently resolved with US-style rules, so a definition body ran on to the
200
+ end of the document. A boundary-heading heuristic also stops an inline
201
+ lowercase continuation ("…disclosed under\nClause 2 excluding…") from
202
+ ending a definition.
203
+ - `ReferenceDetector.resolve()` re-derives a reference's container kind from
204
+ its raw text when `target_kind` is still the default `"clause"`. A
205
+ `CrossReference` built by hand — by an SDK integrator feeding references
206
+ from an external extractor — and passed straight to `resolve()` used to
207
+ match a main-body `clause 1` for `"Schedule 1"` and report
208
+ `target_kind="clause"`. Bare identifiers (`"Article VII"`) are unaffected.
209
+ - `FallbackChunker` splits a sentence longer than `max_chunk_size` at word
210
+ boundaries before assembling chunks, preserving exact character offsets. A
211
+ run-on sentence with no internal punctuation — plausible in OCR'd or
212
+ poorly formatted text — used to be emitted whole, violating the
213
+ `max_chunk_size` hard cap. A single indivisible word over the budget is
214
+ still emitted as-is.
215
+ - Cross-references to a sub-clause that was merged into a larger chunk
216
+ (`clause 2.7`, `clause 3.4(b)`) now resolve: the clause-aware path hands
217
+ the absorbed identifiers to the resolver instead of dropping them.
218
+ - A merged chunk no longer registers its own identifier twice, which made it
219
+ look ambiguous with itself and left its references unresolved.
220
+ - The pieces of an over-sized clause keep the clause's own identifier —
221
+ `hierarchy`, `hierarchy_path`, `original_header` and `context_header` no
222
+ longer expose the internal `.__part<n>` suffix. Uniqueness comes from the
223
+ internal `uid`, and a reference to the clause resolves to the piece where
224
+ it starts.
225
+ - `(i)` roman-numeral sub-clause identifiers are no longer misdetected or
226
+ mis-normalised during cross-reference resolution.
227
+ - A dash between two references is only treated as a range operator when
228
+ there is evidence for it, so `Clause 5 - Payment` is no longer read as the
229
+ range `Clause 5` to `Payment`.
230
+ - EU `Chapter`-level sections are no longer misclassified into
231
+ `DocumentSection.SCHEDULES`; document-section keyword classification is
232
+ anchored to the heading rather than matched anywhere in the body.
233
+ - The heading-plausibility gate no longer swallows real clause headings.
234
+ - The UK `next_header_re` Schedule branch was missing a capture group, so
235
+ Schedule boundaries were mis-detected.
236
+ - Structure parsing derives line offsets from the same splitter `parse()`
237
+ uses, so `char_start`/`char_end` cannot drift from the parsed lines.
238
+ - Definition extraction: the defined-term character class was widened and
239
+ opening quotes anchored (so `"Level 2 Data"` is found), a definition body
240
+ is bounded at the next entry marker — including a US/EU-style
241
+ `Section 1.2` / `Article II` marker — or at an ALL-CAPS operative heading,
242
+ so neighbouring entries no longer leak into the previous definition.
243
+ - `LegalChunk.from_dict()` validates every container field instead of
244
+ trusting the input shape.
245
+ - The definition cache key tolerates unpaired surrogates instead of raising
246
+ `UnicodeEncodeError`, and registry names are type-checked.
247
+ - `LegalTextSplitter.split_documents()` ids are unique across the whole
248
+ call. `chunk.index` restarts at 0 for each input `Document`, so under the
249
+ common one-`Document`-per-page loader pattern every page shares
250
+ `metadata["source"]` and `f"{document_id}:{chunk.index}"` collided —
251
+ a vector store's `add_documents` upsert then kept only the last chunk per
252
+ id and silently dropped the rest.
253
+ - `LegalNodeParser` derives a stable document identifier from content when
254
+ the caller supplied none. LlamaIndex fills an unnamed `Document.id_` with
255
+ a fresh `uuid4`, which made the context header, embed text and `node_id`
256
+ differ on every run and duplicated every vector on re-ingestion.
257
+ - `examples/` now passes `ruff check` (previously 7 errors — unnecessary
258
+ f-string prefixes and an unused local); CI lints
259
+ `src/ tests/ examples/ benchmarks/` instead of `src/ tests/` only.
260
+ - **`jurisdiction="us"` now recognises bare-decimal headings**
261
+ (`1. Definitions.` / `1.1` / `1.1.1`), the dominant US commercial drafting
262
+ style. Previously the `us` profile required a literal `Section` or `ARTICLE`
263
+ marker, so on a 150-contract CUAD sample it recovered five or more top-level
264
+ clauses in 14% of contracts against 31% for `uk` on the *same* US filings —
265
+ the profile named for the jurisdiction was the worse choice for it, and
266
+ anyone passing `us` silently got fixed-size splitting.
267
+ - **`max_chunk_size` is now a hard cap on every path.** A run offering no
268
+ split point — an OCR'd table, a base64 blob, a 2,485-character line — was
269
+ emitted whole, up to 2.4x over the limit (1,220 tokens against a configured
270
+ 512). The cascading splitter gains a final character-window level, and the
271
+ fallback path now uses the same splitter as the clause-aware path instead of
272
+ treating a sentence as indivisible. When a run offers no boundary of any
273
+ kind inside the budget the cut lands mid-word, and that is logged once at
274
+ `WARNING`.
275
+ - **Consecutive chunks tile the document on the fallback path.** Sentences
276
+ were stripped before their offsets were recorded, so the whitespace between
277
+ two sentences belonged to neither chunk and consecutive spans were one
278
+ character apart.
279
+ - **Wrapped sentences are no longer read as headings.** A heading candidate
280
+ must now open a block — the previous line blank, ending a sentence, or
281
+ itself a heading. Text hard-wrapped out of a PDF put `Schedule 2.`,
282
+ `Section 2.04.` and `7.2. Continued use ...` at the head of a line, each the
283
+ tail of a sentence; believing one re-parents the rest of the document under
284
+ a clause that is not there.
285
+ - **Letter-named attachments resolve.** `Exhibit A` and `Schedule B` are now
286
+ detected as cross-references under `us`. Previously only digits and Roman
287
+ numerals were accepted, so `Exhibit C` resolved (C is a Roman numeral) while
288
+ `Exhibit A`, `B` and `D` did not.
289
+
290
+ ### Performance
291
+ - **Definition-body extraction is linear in the document again.** For each
292
+ definition it searched from that definition to the end of the text, once per
293
+ stop pattern, with around fourteen patterns — O(definitions x length). On
294
+ the worst CUAD contract (291,873 characters, 414 definitions) chunking took
295
+ 20.2 s against a 0.042 s corpus median. Bounding each search to the best
296
+ boundary found so far takes that to **0.84 s**, with byte-identical output
297
+ on every fixture and both CUAD contracts. A second contract went 0.70 s to
298
+ 0.08 s. The registry-driven blank-line-then-header stop condition is bounded
299
+ the same way.
300
+
301
+ ### Changed
302
+ - `chunk.content` is documented, prominently, as **not** being
303
+ `sanitized_text[char_start:char_end]` by default. It never was — measured on
304
+ 60 real CUAD contracts, 51% of chunks carry a prepended ancestor heading —
305
+ but the contract was stated nowhere, and `include_context_header=False` does
306
+ not change it (that flag governs the separate `context_header` field). See
307
+ the `LegalChunk` docstring and `docs/architecture.md`.
308
+ - The `us_msa` snapshot gains two chunks (48 to 50) and three resolved
309
+ cross-references (71 to 74). Bare-decimal heading recognition now finds the
310
+ numbered clauses inside `EXHIBIT A`, so the exhibit's body is split into its
311
+ own clauses instead of one flat chunk, and the letter-named `Exhibit A`
312
+ references resolve to it.
313
+
314
+ ## [0.8.0b1] — 2026-03-17
315
+
316
+ ### Added
317
+ - EU Directives jurisdiction (`Jurisdiction.EU` / `"eu"`) — supports GDPR, DSA, DMA, AI Act, ePrivacy structure (Chapter/Article/Section/paragraph/Annex)
318
+ - GDPR test fixture (`tests/fixtures/eu_gdpr_excerpt.txt`) with 10 fixture-based tests
319
+ - ReDoS security audit — 28 tests verifying all regex patterns resist catastrophic backtracking with pathological inputs
320
+ - Coverage enforcement in CI — `--cov-fail-under=90` gate (currently 97%)
321
+ - PyPI publish workflow (`.github/workflows/publish.yml`) — automated release on tag push via OIDC trusted publisher
322
+ - Character offset invariant tests (`tests/test_char_offsets.py`) — 16 tests covering all chunking paths
323
+ - Lowercase-initial defined term support (`"the Company" means...`)
324
+ - Roman/Arabic numeral normalization for cross-reference resolution
325
+ - `max_cache_size` parameter on `LegalChunker` (default 128, FIFO eviction)
326
+ - Pipeline stage invariant documentation in `_run_pipeline()`
327
+
328
+ ### Fixed
329
+ - Critical: `_split_oversized_clause` produced negative `char_start` values — rewrote offset tracking
330
+ - `bisect_left` tuple comparison edge case in `_nearest_clause_label` — replaced with flat-list `bisect_right`
331
+ - EU `_find_section_end` missing numbered paragraph boundary detection
332
+ - Definitions section header matching too broad — added word boundary constraints
333
+ - Hereinafter definition lookback window increased from 200 to 500 chars
334
+
335
+ ### Changed
336
+ - Version bumped to 0.8.0b1 (Beta status)
337
+ - Development Status classifier upgraded from Alpha to Beta
338
+ - CI now runs `pytest --cov` with 90% minimum coverage gate
339
+
340
+ ## [0.7.0] — 2026-03-14
341
+
342
+ ### Added
343
+ - `PipelineMetrics` and `StageMetric` frozen dataclasses for pipeline observability
344
+ - `LegalChunker.chunk_with_metrics()` — returns `(chunks, metrics)` with per-stage wall-clock timing
345
+ - Per-stage structured logging at DEBUG level (stage start/done with item counts and timing)
346
+ - Developer documentation: `docs/architecture.md` (pipeline design) and `docs/extending.md` (custom jurisdictions, clause signals)
347
+ - This changelog
348
+
349
+ ### Changed
350
+ - Internal pipeline logic extracted into `_run_pipeline()` shared by `chunk()` and `chunk_with_metrics()`
351
+ - `chunk()` behaviour is unchanged — zero overhead when metrics are not requested
352
+
353
+ ## [0.6.0] — 2026-03-14
354
+
355
+ ### Added
356
+ - Classification confidence scoring (`classification_confidence`, `secondary_clause_type` on `LegalChunk`)
357
+ - `ClassificationResult` frozen dataclass with `MappingProxyType` scores
358
+ - Position-aware clause type scoring (+1.5 bonus for end-of-document types past 75%)
359
+ - `ClauseTypeClassifier.classify_detailed()` public method
360
+ - "Hereinafter" inline definition extraction with preceding-context support
361
+ - Cross-reference resolution stats (`cross_ref_total`, `cross_ref_resolved` per chunk)
362
+ - `LegalChunker.cross_ref_resolution_rate` and `cross_ref_stats` properties
363
+
364
+ ## [0.5.0] — 2026-03-14
365
+
366
+ ### Added
367
+ - SHA-256-keyed definition extraction cache (`enable_definition_cache` param)
368
+ - `LegalChunker.clear_definition_cache()` method
369
+ - `chunk_iter()` generator wrapper
370
+ - `chunk_batch()` with serial and parallel (`ProcessPoolExecutor`) paths
371
+ - `BatchResult` and `BatchError` dataclasses
372
+ - Platform-aware worker cap (Windows: max 61)
373
+ - Performance benchmarks (`benchmarks/`)
374
+
375
+ ## [0.4.0] — 2026-03-14
376
+
377
+ ### Added
378
+ - `JurisdictionPatterns` `@runtime_checkable` Protocol
379
+ - Jurisdiction registry: `register_jurisdiction()` for custom jurisdictions
380
+ - `extra_clause_signals` parameter on `LegalChunker` for custom classification keywords
381
+ - `_merge_signals()` helper — never mutates built-in `CLAUSE_SIGNALS`
382
+
383
+ ## [0.3.0] — 2026-03-14
384
+
385
+ ### Added
386
+ - Exception hierarchy: `LexichunkError` -> `ConfigurationError`, `ParsingError`, `InputError`
387
+ - Input sanitization: BOM stripping, CRLF normalization, null byte removal, NFC normalization
388
+ - Expanded legal abbreviation support (~110 abbreviations across 7 categories)
389
+ - `extra_abbreviations` parameter on `FallbackChunker` and `LegalChunker`
390
+ - Hypothesis property-based tests
391
+
392
+ ## [0.2.0] — 2026-03-14
393
+
394
+ ### Added
395
+ - `original_header` field on `LegalChunk`
396
+ - Ancestor header prepending for hierarchy context
397
+ - Content fidelity raised from 85% to 99%
398
+
399
+ ### Fixed
400
+ - `_split_oversized_clause()` no longer drops parent content when children exist
401
+
402
+ ## [0.1.0] — 2026-03-14
403
+
404
+ ### Added
405
+ - Initial release
406
+ - 8-stage pipeline: structure parsing, chunking, cross-reference detection, clause type classification, context enrichment, defined terms, cross-reference resolution
407
+ - UK and US jurisdiction support
408
+ - `LegalChunker` public API with `chunk()`, `get_defined_terms()`, `parse_structure()`
409
+ - LangChain (`LegalTextSplitter`) and LlamaIndex (`LegalNodeParser`) integrations
410
+ - 107 tests
@@ -0,0 +1,75 @@
1
+ # Contributing
2
+
3
+ ## Setup
4
+
5
+ Using [uv](https://docs.astral.sh/uv/) (recommended):
6
+
7
+ ```bash
8
+ git clone https://github.com/emmcygn/lexichunk
9
+ cd lexichunk
10
+ uv venv
11
+ uv pip install -e ".[dev,all]"
12
+ ```
13
+
14
+ Using plain `pip`:
15
+
16
+ ```bash
17
+ git clone https://github.com/emmcygn/lexichunk
18
+ cd lexichunk
19
+ python -m venv .venv
20
+ # Windows: .venv\Scripts\activate | macOS/Linux: source .venv/bin/activate
21
+ pip install -e ".[dev,all]"
22
+ ```
23
+
24
+ ## Running the gates
25
+
26
+ Run these before opening a pull request — CI runs the same checks on Python
27
+ 3.10 through 3.13, plus Windows on 3.12:
28
+
29
+ ```bash
30
+ ruff check src/ tests/ examples/ benchmarks/
31
+ mypy src/lexichunk/
32
+ pytest --cov=lexichunk --cov-fail-under=92
33
+ pytest benchmarks --benchmark-disable
34
+ ```
35
+
36
+ `ruff format` is **not** enforced — the tree is not currently formatted with
37
+ it, and reformatting everything would bury real changes in noise. Match the
38
+ style of the file you are editing.
39
+
40
+ ## Property-based tests
41
+
42
+ `tests/test_properties.py` and `tests/test_invariants.py` use Hypothesis.
43
+ `tests/conftest.py` registers two derandomised profiles, so the same commit
44
+ generates the same examples everywhere:
45
+
46
+ - `dev` (default locally) — 500 ms per-example deadline;
47
+ - `ci` (used when `$CI` is set, or via `HYPOTHESIS_PROFILE=ci`) — 100 examples
48
+ and a 2 s deadline, for cold shared runners.
49
+
50
+ To reproduce a CI failure locally, run `HYPOTHESIS_PROFILE=ci pytest ...`.
51
+
52
+ ## Documentation is tested
53
+
54
+ `tests/test_readme_examples.py` executes every ```` ```python ```` block in
55
+ `README.md`. If you change the API, update the README in the same PR or that
56
+ test fails. A block that genuinely cannot run in CI needs an HTML comment
57
+ above it containing `lexichunk-doctest: skip` **and a stated reason**.
58
+
59
+ `tests/test_docstrings.py` requires a docstring on every name in
60
+ `lexichunk.__all__` and every public member of `LegalChunker`, and pins the
61
+ `LegalChunker` public surface — adding a public method means updating that
62
+ list and `CHANGELOG.md` deliberately.
63
+
64
+ ## Updating snapshots
65
+
66
+ Some tests compare output against golden files in `tests/snapshots/*.json`.
67
+ If your change intentionally alters chunker output, regenerate them with:
68
+
69
+ ```bash
70
+ pytest --update-snapshots
71
+ ```
72
+
73
+ **Always review the resulting JSON diff and explain it in the PR** — a
74
+ snapshot update is only acceptable when the change is intentional and the
75
+ diff has been inspected line by line, not merely to make a failing test pass.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Emmanuel Cuyugan
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.
@@ -0,0 +1,5 @@
1
+ recursive-include tests *.py *.txt *.json
2
+ recursive-include docs *.md
3
+ recursive-include examples *.py
4
+ recursive-include benchmarks *.py
5
+ include LICENSE README.md CHANGELOG.md SECURITY.md CONTRIBUTING.md