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.
- lexichunk-0.9.0/CHANGELOG.md +410 -0
- lexichunk-0.9.0/CONTRIBUTING.md +75 -0
- lexichunk-0.9.0/LICENSE +21 -0
- lexichunk-0.9.0/MANIFEST.in +5 -0
- lexichunk-0.9.0/PKG-INFO +881 -0
- lexichunk-0.9.0/README.md +831 -0
- lexichunk-0.9.0/SECURITY.md +69 -0
- lexichunk-0.9.0/benchmarks/conftest.py +29 -0
- lexichunk-0.9.0/benchmarks/test_perf_chunk.py +81 -0
- lexichunk-0.9.0/docs/adoption-guide.md +114 -0
- lexichunk-0.9.0/docs/architecture.md +333 -0
- lexichunk-0.9.0/docs/extending.md +170 -0
- lexichunk-0.9.0/docs/ingestion.md +236 -0
- lexichunk-0.9.0/examples/basic_usage.py +208 -0
- lexichunk-0.9.0/examples/compare_chunkers.py +407 -0
- lexichunk-0.9.0/examples/docling_pipeline.py +216 -0
- lexichunk-0.9.0/examples/langchain_rag.py +233 -0
- lexichunk-0.9.0/examples/llm_fallback.py +164 -0
- lexichunk-0.9.0/examples/offline_evidence_retrieval.py +105 -0
- lexichunk-0.9.0/pyproject.toml +111 -0
- lexichunk-0.9.0/setup.cfg +4 -0
- lexichunk-0.9.0/src/lexichunk/__init__.py +66 -0
- lexichunk-0.9.0/src/lexichunk/_patterns.py +66 -0
- lexichunk-0.9.0/src/lexichunk/chunker.py +2049 -0
- lexichunk-0.9.0/src/lexichunk/documents.py +571 -0
- lexichunk-0.9.0/src/lexichunk/enrichment/__init__.py +1 -0
- lexichunk-0.9.0/src/lexichunk/enrichment/clause_type.py +660 -0
- lexichunk-0.9.0/src/lexichunk/enrichment/context.py +152 -0
- lexichunk-0.9.0/src/lexichunk/exceptions.py +25 -0
- lexichunk-0.9.0/src/lexichunk/ingestion/__init__.py +49 -0
- lexichunk-0.9.0/src/lexichunk/ingestion/_common.py +240 -0
- lexichunk-0.9.0/src/lexichunk/ingestion/docling.py +258 -0
- lexichunk-0.9.0/src/lexichunk/ingestion/markdown.py +111 -0
- lexichunk-0.9.0/src/lexichunk/ingestion/unstructured.py +171 -0
- lexichunk-0.9.0/src/lexichunk/integrations/__init__.py +1 -0
- lexichunk-0.9.0/src/lexichunk/integrations/langchain.py +361 -0
- lexichunk-0.9.0/src/lexichunk/integrations/llama_index.py +440 -0
- lexichunk-0.9.0/src/lexichunk/jurisdiction/__init__.py +207 -0
- lexichunk-0.9.0/src/lexichunk/jurisdiction/eu.py +182 -0
- lexichunk-0.9.0/src/lexichunk/jurisdiction/uk.py +144 -0
- lexichunk-0.9.0/src/lexichunk/jurisdiction/us.py +259 -0
- lexichunk-0.9.0/src/lexichunk/metrics.py +106 -0
- lexichunk-0.9.0/src/lexichunk/models.py +661 -0
- lexichunk-0.9.0/src/lexichunk/offsets.py +369 -0
- lexichunk-0.9.0/src/lexichunk/parsers/__init__.py +15 -0
- lexichunk-0.9.0/src/lexichunk/parsers/definitions.py +831 -0
- lexichunk-0.9.0/src/lexichunk/parsers/references.py +954 -0
- lexichunk-0.9.0/src/lexichunk/parsers/structure.py +1352 -0
- lexichunk-0.9.0/src/lexichunk/py.typed +0 -0
- lexichunk-0.9.0/src/lexichunk/strategies/__init__.py +34 -0
- lexichunk-0.9.0/src/lexichunk/strategies/_cascade.py +186 -0
- lexichunk-0.9.0/src/lexichunk/strategies/clause_aware.py +923 -0
- lexichunk-0.9.0/src/lexichunk/strategies/fallback.py +448 -0
- lexichunk-0.9.0/src/lexichunk/utils.py +92 -0
- lexichunk-0.9.0/src/lexichunk.egg-info/PKG-INFO +881 -0
- lexichunk-0.9.0/src/lexichunk.egg-info/SOURCES.txt +141 -0
- lexichunk-0.9.0/src/lexichunk.egg-info/dependency_links.txt +1 -0
- lexichunk-0.9.0/src/lexichunk.egg-info/requires.txt +27 -0
- lexichunk-0.9.0/src/lexichunk.egg-info/top_level.txt +1 -0
- lexichunk-0.9.0/tests/__init__.py +0 -0
- lexichunk-0.9.0/tests/_snapshot_support.py +106 -0
- lexichunk-0.9.0/tests/conftest.py +134 -0
- lexichunk-0.9.0/tests/fixtures/eu_gdpr_excerpt.txt +75 -0
- lexichunk-0.9.0/tests/fixtures/generators/_emit.py +643 -0
- lexichunk-0.9.0/tests/fixtures/generators/generate_uk_pdf_extracted_agreement.py +1259 -0
- lexichunk-0.9.0/tests/fixtures/generators/generate_us_msa_signed_with_exhibits.py +1056 -0
- lexichunk-0.9.0/tests/fixtures/gold/uk_pdf_extracted_agreement.json +1346 -0
- lexichunk-0.9.0/tests/fixtures/gold/us_msa_signed_with_exhibits.json +934 -0
- lexichunk-0.9.0/tests/fixtures/uk_pdf_extracted_agreement.txt +664 -0
- lexichunk-0.9.0/tests/fixtures/uk_service_agreement.txt +484 -0
- lexichunk-0.9.0/tests/fixtures/uk_terms_conditions.txt +316 -0
- lexichunk-0.9.0/tests/fixtures/us_msa.txt +422 -0
- lexichunk-0.9.0/tests/fixtures/us_msa_signed_with_exhibits.txt +523 -0
- lexichunk-0.9.0/tests/fixtures/us_terms_of_service.txt +420 -0
- lexichunk-0.9.0/tests/snapshots/eu_gdpr_excerpt.json +240 -0
- lexichunk-0.9.0/tests/snapshots/eu_gdpr_excerpt.summary.json +7 -0
- lexichunk-0.9.0/tests/snapshots/uk_pdf_extracted_agreement.json +1431 -0
- lexichunk-0.9.0/tests/snapshots/uk_pdf_extracted_agreement.summary.json +7 -0
- lexichunk-0.9.0/tests/snapshots/uk_service_agreement.json +1271 -0
- lexichunk-0.9.0/tests/snapshots/uk_service_agreement.summary.json +7 -0
- lexichunk-0.9.0/tests/snapshots/uk_terms_conditions.json +813 -0
- lexichunk-0.9.0/tests/snapshots/uk_terms_conditions.summary.json +7 -0
- lexichunk-0.9.0/tests/snapshots/us_msa.json +1504 -0
- lexichunk-0.9.0/tests/snapshots/us_msa.summary.json +7 -0
- lexichunk-0.9.0/tests/snapshots/us_msa_signed_with_exhibits.json +1366 -0
- lexichunk-0.9.0/tests/snapshots/us_msa_signed_with_exhibits.summary.json +7 -0
- lexichunk-0.9.0/tests/snapshots/us_terms_of_service.json +1471 -0
- lexichunk-0.9.0/tests/snapshots/us_terms_of_service.summary.json +7 -0
- lexichunk-0.9.0/tests/test_abbreviations.py +238 -0
- lexichunk-0.9.0/tests/test_adversarial_extensibility.py +590 -0
- lexichunk-0.9.0/tests/test_adversarial_v050.py +459 -0
- lexichunk-0.9.0/tests/test_adversarial_v060.py +318 -0
- lexichunk-0.9.0/tests/test_adversarial_v070.py +281 -0
- lexichunk-0.9.0/tests/test_adversarial_v080.py +262 -0
- lexichunk-0.9.0/tests/test_ancestor_headers.py +127 -0
- lexichunk-0.9.0/tests/test_batch.py +177 -0
- lexichunk-0.9.0/tests/test_char_offsets.py +268 -0
- lexichunk-0.9.0/tests/test_chunk_documents.py +485 -0
- lexichunk-0.9.0/tests/test_chunk_iter.py +47 -0
- lexichunk-0.9.0/tests/test_chunker.py +981 -0
- lexichunk-0.9.0/tests/test_classification_confidence.py +151 -0
- lexichunk-0.9.0/tests/test_classification_hook.py +388 -0
- lexichunk-0.9.0/tests/test_clause_types.py +330 -0
- lexichunk-0.9.0/tests/test_context_enricher.py +123 -0
- lexichunk-0.9.0/tests/test_crossref_stats.py +136 -0
- lexichunk-0.9.0/tests/test_definition_cache.py +96 -0
- lexichunk-0.9.0/tests/test_definitions.py +429 -0
- lexichunk-0.9.0/tests/test_definitions_performance.py +147 -0
- lexichunk-0.9.0/tests/test_docstrings.py +113 -0
- lexichunk-0.9.0/tests/test_eu_jurisdiction.py +358 -0
- lexichunk-0.9.0/tests/test_exceptions.py +161 -0
- lexichunk-0.9.0/tests/test_exhibit_letter_references.py +122 -0
- lexichunk-0.9.0/tests/test_extra_clause_signals.py +104 -0
- lexichunk-0.9.0/tests/test_g1_structure.py +586 -0
- lexichunk-0.9.0/tests/test_g2_strategies.py +440 -0
- lexichunk-0.9.0/tests/test_g3_parsers.py +679 -0
- lexichunk-0.9.0/tests/test_g4a_api.py +813 -0
- lexichunk-0.9.0/tests/test_g4b_integrations.py +581 -0
- lexichunk-0.9.0/tests/test_g6_polish.py +521 -0
- lexichunk-0.9.0/tests/test_g7_adversarial_fixes.py +1231 -0
- lexichunk-0.9.0/tests/test_gold_fixtures.py +566 -0
- lexichunk-0.9.0/tests/test_heading_regression.py +233 -0
- lexichunk-0.9.0/tests/test_heading_shapes.py +211 -0
- lexichunk-0.9.0/tests/test_ingestion.py +606 -0
- lexichunk-0.9.0/tests/test_integrations.py +482 -0
- lexichunk-0.9.0/tests/test_invariants.py +382 -0
- lexichunk-0.9.0/tests/test_jurisdiction_registry.py +182 -0
- lexichunk-0.9.0/tests/test_metrics.py +285 -0
- lexichunk-0.9.0/tests/test_offline_example.py +13 -0
- lexichunk-0.9.0/tests/test_offset_map.py +367 -0
- lexichunk-0.9.0/tests/test_position_scoring.py +134 -0
- lexichunk-0.9.0/tests/test_properties.py +145 -0
- lexichunk-0.9.0/tests/test_random_invariants.py +356 -0
- lexichunk-0.9.0/tests/test_readme_examples.py +180 -0
- lexichunk-0.9.0/tests/test_redos_audit.py +239 -0
- lexichunk-0.9.0/tests/test_references.py +378 -0
- lexichunk-0.9.0/tests/test_release_workflows.py +91 -0
- lexichunk-0.9.0/tests/test_sanitization.py +154 -0
- lexichunk-0.9.0/tests/test_serialization.py +221 -0
- lexichunk-0.9.0/tests/test_snapshots.py +101 -0
- lexichunk-0.9.0/tests/test_structure_metrics.py +255 -0
- lexichunk-0.9.0/tests/test_structure_parser.py +373 -0
- 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.
|
lexichunk-0.9.0/LICENSE
ADDED
|
@@ -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.
|