jsonl-diff 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rolando Morales Perez
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,548 @@
1
+ Metadata-Version: 2.4
2
+ Name: jsonl-diff
3
+ Version: 0.1.0
4
+ Summary: Strict, disk-backed reconciliation of large JSONL/NDJSON datasets for Python.
5
+ Author-email: rmoralespp <rmoralespp@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Rolando Morales Perez
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Source, https://github.com/rmoralespp/jsonl-diff
29
+ Project-URL: Homepage, https://github.com/rmoralespp/jsonl-diff
30
+ Project-URL: Changelog, https://github.com/rmoralespp/jsonl-diff/blob/main/CHANGELOG.md
31
+ Project-URL: Issues, https://github.com/rmoralespp/jsonl-diff/issues
32
+ Project-URL: Documentation, https://rmoralespp.github.io/jsonl-diff/
33
+ Keywords: jsonlines,ndjson,jsonl,json,gzip,bzip2,xz,zst,utilities,serialization,deserialization,diff,json-diff,cli
34
+ Classifier: Programming Language :: Python
35
+ Classifier: Programming Language :: Python :: 3 :: Only
36
+ Classifier: Programming Language :: Python :: 3.8
37
+ Classifier: Programming Language :: Python :: 3.9
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
42
+ Classifier: Programming Language :: Python :: 3.14
43
+ Classifier: Intended Audience :: Developers
44
+ Classifier: Intended Audience :: Information Technology
45
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
46
+ Classifier: Topic :: Internet
47
+ Classifier: Topic :: Utilities
48
+ Classifier: Topic :: File Formats :: JSON
49
+ Requires-Python: >=3.8
50
+ Description-Content-Type: text/markdown
51
+ License-File: LICENSE
52
+ Requires-Dist: py-jsonl==1.5.2
53
+ Dynamic: license-file
54
+
55
+ # jsonl-diff
56
+
57
+ ----
58
+
59
+ <p>
60
+ <img src="https://img.shields.io/pypi/v/jsonl-diff.svg" alt="PyPI">
61
+ <img src="https://img.shields.io/pypi/pyversions/jsonl-diff.svg" alt="Python">
62
+ <img src="https://github.com/rmoralespp/jsonl-diff/workflows/CI/badge.svg" alt="CI">
63
+ <img src="https://codecov.io/gh/rmoralespp/jsonl-diff/branch/main/graph/badge.svg" alt="Coverage">
64
+ <img src="https://img.shields.io/github/license/rmoralespp/jsonl-diff.svg" alt="License">
65
+ </p>
66
+
67
+ `jsonl-diff` performs strict, identity-based reconciliation of JSONL/NDJSON
68
+ datasets. It matches records by one or more top-level fields, then classifies
69
+ each identity as `equal`, `added`, `deleted`, or `modified`.
70
+
71
+ ## Why jsonl-diff?
72
+
73
+ A text diff compares lines. That is usually the wrong model for datasets:
74
+ reordering unchanged records creates noise, and inserting one record can make
75
+ every later line appear different.
76
+
77
+ `jsonl-diff` fills the gap between line-oriented tools and in-memory dataframe comparisons:
78
+
79
+ - match records by identity, not physical position;
80
+ - compare datasets larger than available RAM using temporary SQLite storage;
81
+ - preserve original OLD and NEW line numbers for auditability;
82
+ - expose the same behavior through a CLI and a small Python API.
83
+
84
+ Use it for snapshots, ETL validation, migrations, exports, and CI checks. Use
85
+ `diff`, `jq`, or a visual JSON diff instead when you need line-level,
86
+ field-level, or JSON Patch output.
87
+
88
+ The comparison is backed by a temporary SQLite index. Complete inputs and
89
+ complete result sets do not need to fit in memory. Process memory is bounded by
90
+ the largest record plus parser and SQLite buffers, while indexed data lives on
91
+ temporary disk.
92
+
93
+ ## Features
94
+
95
+ - Single or composite top-level identities, independent of input order.
96
+ - Configurable duplicate handling with strict failure by default.
97
+ - Exact RFC 6901 object-member ignores.
98
+ - Semantic number comparison without binary floating-point rounding.
99
+ - Deterministic summaries and changed-identity iteration.
100
+ - Original OLD and NEW physical line numbers for every change.
101
+ - Local, HTTP/HTTPS, file-like, and supported compressed sources through
102
+ `py-jsonl`.
103
+ - The same comparison engine through the CLI and Python API.
104
+ - Disk-backed comparison with configurable `jsonl-diff` temporary storage.
105
+
106
+ ## Requirements and installation
107
+
108
+ `jsonl-diff` supports Python 3.8 through 3.14.
109
+
110
+ Install it in a project managed by `uv`:
111
+
112
+ ```bash
113
+ uv add jsonl-diff
114
+ ```
115
+
116
+ Install the CLI as a standalone tool:
117
+
118
+ ```bash
119
+ uv tool install jsonl-diff
120
+ ```
121
+
122
+ `pip` is also supported:
123
+
124
+ ```bash
125
+ python -m pip install jsonl-diff
126
+ ```
127
+
128
+ ## CLI quick start
129
+
130
+ Given:
131
+
132
+ ```jsonl
133
+ {"id":1,"name":"Ada"}
134
+ {"id":2,"name":"Grace"}
135
+ ```
136
+
137
+ and a reordered, changed dataset:
138
+
139
+ ```jsonl
140
+ {"id":2,"name":"Grace Hopper"}
141
+ {"name":"Ada","id":1}
142
+ {"id":3,"name":"Linus"}
143
+ ```
144
+
145
+ compare them by `id`:
146
+
147
+ ```bash
148
+ jsonl-diff old.jsonl new.jsonl --key id
149
+ ```
150
+
151
+ Standard output contains only the text summary:
152
+
153
+ ```text
154
+ Records:
155
+ equal: 1
156
+ added: 1
157
+ deleted: 0
158
+ modified: 1
159
+ ```
160
+
161
+ The result means one record is unchanged, one was added, and one existing
162
+ record changed. Physical order does not affect these counts.
163
+
164
+ Diagnostics are written to standard error. Use `--quiet` when only the exit
165
+ status or details file is needed.
166
+
167
+ ### Common pipelines
168
+
169
+ Filter JSONL with `jq` before comparing it. `-c` keeps one compact JSON object
170
+ per line:
171
+
172
+ ```bash
173
+ jq -c 'select(has("deleted_at") and .deleted_at == null)' old.jsonl |
174
+ jsonl-diff - new.jsonl --key id
175
+ ```
176
+
177
+ Read compressed input directly when no preprocessing is required:
178
+
179
+ ```bash
180
+ jsonl-diff old.jsonl.xz new.jsonl.gz --key id
181
+ ```
182
+
183
+ For two independent pipelines on Bash-compatible systems, use process
184
+ substitution:
185
+
186
+ ```bash
187
+ jsonl-diff \
188
+ <(head -n 1000 old.jsonl | jq -c 'select(.active)') \
189
+ <(head -n 1000 new.jsonl | jq -c 'select(.active)') \
190
+ --key id
191
+ ```
192
+
193
+ `-` represents stdin. Both inputs cannot use the same stdin stream.
194
+
195
+ ### CLI syntax and options
196
+
197
+ ```text
198
+ jsonl-diff [-h] --key KEY [--ignore IGNORE]
199
+ [--duplicates {error,first,last}] [--details FILE] [--quiet]
200
+ [--max-temp MAX_TEMP]
201
+ old new
202
+ ```
203
+
204
+ | Argument | Meaning |
205
+ |---|---|
206
+ | `old` | OLD local path, HTTP/HTTPS source, or `-` for stdin. |
207
+ | `new` | NEW local path, HTTP/HTTPS source, or `-` for stdin. |
208
+ | `--key KEY` | Required top-level identity field. Repeat it or use a comma-separated value for a composite identity. |
209
+ | `--ignore JSON_POINTER` | Exact RFC 6901 pointer to an object member to remove before content comparison. Repeatable. |
210
+ | `--duplicates POLICY` | Handle repeated identities with `error` (default), `first`, or `last`. |
211
+ | `--details FILE` | Write machine-readable JSONL details to `FILE`; details are never written to stdout. |
212
+ | `--quiet` | Suppress the normal stdout summary. Errors still go to stderr. |
213
+ | `--max-temp MAX_TEMP` | Limit storage owned by `jsonl-diff` to a positive integer number of bytes. |
214
+ | `-h`, `--help` | Show command help and exit. |
215
+
216
+ These composite-key forms are equivalent:
217
+
218
+ ```bash
219
+ jsonl-diff old.jsonl new.jsonl --key country,customerId,type
220
+
221
+ jsonl-diff old.jsonl new.jsonl \
222
+ --key country \
223
+ --key customerId \
224
+ --key type
225
+ ```
226
+
227
+ Ignore volatile fields by exact path:
228
+
229
+ ```bash
230
+ jsonl-diff old.jsonl new.jsonl \
231
+ --key id \
232
+ --ignore /updated_at \
233
+ --ignore /metadata/request_id
234
+ ```
235
+
236
+ ### Exit codes
237
+
238
+ | Code | Meaning |
239
+ |---:|---|
240
+ | `0` | The inputs are equal under the configured rules. |
241
+ | `1` | At least one added, deleted, or modified identity was found. |
242
+ | `2` | A handled input, source, resource, output, or interruption error occurred. |
243
+ | `3` | CLI usage or comparison configuration is invalid. |
244
+
245
+ Invalid argparse usage exits directly with code `3`. The Python API uses
246
+ exceptions instead of these process exit codes.
247
+
248
+ ## Details JSONL
249
+
250
+ `--details FILE` streams a deterministic JSONL report through `py-jsonl` after
251
+ both inputs have been completely indexed and validated:
252
+
253
+ ```bash
254
+ jsonl-diff old.jsonl new.jsonl --key id --details changes.jsonl
255
+ ```
256
+
257
+ For an OLD source containing identities `1` and `2`, and a NEW source
258
+ containing changed identity `2` and added identity `3`, the file is:
259
+
260
+ ```jsonl
261
+ {"duplicates":"error","ignore":[],"key":["id"],"type":"meta"}
262
+ {"key":[1],"old_line":1,"op":"deleted","type":"change"}
263
+ {"key":[2],"new_line":1,"old_line":2,"op":"modified","type":"change"}
264
+ {"key":[3],"new_line":2,"op":"added","type":"change"}
265
+ {"added":1,"deleted":1,"equal":0,"modified":1,"type":"summary"}
266
+ ```
267
+
268
+ Record types are:
269
+
270
+ - **`meta`**: always first. `key` contains normalized identity-field names;
271
+ `ignore` contains the configured ignore pointers.
272
+ - **`change`**: one per changed identity. `op` is `added`, `deleted`, or
273
+ `modified`. `old_line` is present for deletions and modifications;
274
+ `new_line` is present for additions and modifications. Equal records are not
275
+ emitted.
276
+ - **`summary`**: always last after a successful write, with all four totals.
277
+
278
+ The normal stdout summary is unchanged when `--details` is used, unless
279
+ `--quiet` suppresses it. A write failure can leave a partial details file and
280
+ returns exit code `2`.
281
+
282
+ ## Python API
283
+
284
+ ### Comparing sources
285
+
286
+ ```python
287
+ from jsonl_diff import ChangeOperation, diff
288
+
289
+ with diff(
290
+ "old.jsonl.gz",
291
+ "new.jsonl.gz",
292
+ key=("country", "customerId"),
293
+ ignore=("/updated_at",),
294
+ duplicates="error",
295
+ max_temp=2_000_000_000,
296
+ ) as result:
297
+ print(result.summary)
298
+
299
+ for change in result.changes(ChangeOperation.MODIFIED):
300
+ print(change.key, change.old_line, change.new_line)
301
+ ```
302
+
303
+ The callable signature is:
304
+
305
+ ```python
306
+ from typing import Any, Optional, Sequence, Union
307
+
308
+ from jsonl_diff import DiffResult, DuplicatePolicy
309
+
310
+ def diff(
311
+ old: Any,
312
+ new: Any,
313
+ *,
314
+ key: Union[str, Sequence[str]],
315
+ ignore: Sequence[str] = (),
316
+ duplicates: Union[str, DuplicatePolicy] = DuplicatePolicy.ERROR,
317
+ max_temp: Optional[int] = None,
318
+ ) -> DiffResult:
319
+ ...
320
+ ```
321
+
322
+ `diff()` creates a comparison session without opening either source. Entering
323
+ its context fully reads, indexes, and validates both sources before exposing
324
+ the result. `DiffResult.changes()` is then a lazy iterator over the disk-backed
325
+ result rather than a list held in memory.
326
+
327
+ Using `DiffResult` as a context manager is required. It owns the temporary
328
+ resources and removes its private workspace on exit. A result cannot be entered
329
+ more than once. Its summary becomes available after entering and remains
330
+ available after closing; iterating changes requires the context to remain open.
331
+
332
+ ### Result models
333
+
334
+ `result.summary` is an immutable `Summary`:
335
+
336
+ ```python
337
+ Summary(equal=10, added=2, deleted=1, modified=3)
338
+ ```
339
+
340
+ It exposes integer fields `equal`, `added`, `deleted`, and `modified`, plus the
341
+ boolean property `different`. The same values are available directly as
342
+ read-only `DiffResult` properties.
343
+
344
+ Each item from `result.changes()` is an immutable `Change` with:
345
+
346
+ - `operation`: a `ChangeOperation` enum value (`ADDED`, `DELETED`, or
347
+ `MODIFIED`);
348
+ - `key`: a typed tuple in normalized identity-field order;
349
+ - `old` and `new`: optional `SourceLocation` values;
350
+ - `old_line` and `new_line`: convenience properties returning a one-based
351
+ physical line or `None`.
352
+
353
+ For additions, `old`/`old_line` are `None`; for deletions,
354
+ `new`/`new_line` are `None`; modifications have both locations. Location
355
+ sources are labeled `OLD` and `NEW`.
356
+
357
+ Pass a `ChangeOperation` to filter without materializing all changes:
358
+
359
+ ```python
360
+ with diff("old.jsonl", "new.jsonl", key="id") as result:
361
+ added = result.changes(ChangeOperation.ADDED)
362
+ for change in added:
363
+ print(change.key, change.new_line)
364
+ ```
365
+
366
+ With numeric keys, API key components are `decimal.Decimal` values. For
367
+ example, JSON identity `7` is returned as `Decimal("7")`; JSON strings and
368
+ booleans retain their types. Details JSONL writes numeric keys as JSON numbers,
369
+ including arbitrary-precision integers.
370
+
371
+ ### API errors
372
+
373
+ The public error hierarchy starts with `JsonlDiffError`:
374
+
375
+ - `ConfigurationError`: invalid keys, ignore pointers, or `max_temp`;
376
+ - `InputError`: an invalid source or record; exposes `source` and optional
377
+ `line`;
378
+ - `DuplicateKeyError`: an `InputError` with `key` and the first/repeated
379
+ physical lines in `lines`;
380
+ - `ResourceError`: the temporary index cannot be created, written, or kept
381
+ within its configured limit.
382
+
383
+ Failures during `diff()` clean up the workspace before the exception is
384
+ raised.
385
+
386
+ ## Sources and compression
387
+
388
+ Source opening and decompression are delegated to `py-jsonl`:
389
+
390
+ | Source | CLI | Python API |
391
+ |---|:---:|:---:|
392
+ | Local path | Yes | Yes, including path-like values |
393
+ | HTTP/HTTPS URL | Yes | Yes |
394
+ | File-like object | No | Yes |
395
+ | gzip (`.gz`) | Yes | Yes |
396
+ | bzip2 (`.bz2`) | Yes | Yes |
397
+ | xz (`.xz`) | Yes | Yes |
398
+ | Zstandard (`.zst`) | Python 3.14 only | Python 3.14 only |
399
+ | ZIP archive | No | No |
400
+
401
+ Zstandard availability follows `py-jsonl` and its use of Python 3.14's
402
+ standard-library zstd support; it is not supported by this project on earlier
403
+ Python versions. Compression and HTTP transfer boundaries do not affect the
404
+ reported decoded JSONL line numbers.
405
+
406
+ ## Comparison semantics
407
+
408
+ ### Strict JSONL records
409
+
410
+ - Every physical line must be non-blank, valid JSON containing exactly one
411
+ top-level object.
412
+ - Local paths and binary streams are decoded as UTF-8. Text streams are
413
+ already decoded, while URL response charset handling follows `py-jsonl`
414
+ (defaulting to UTF-8 when no charset is declared).
415
+ - Blank lines, malformed JSON, duplicate object property names, `NaN`,
416
+ `Infinity`, and `-Infinity` are errors.
417
+ - Top-level arrays, strings, numbers, booleans, and `null` are rejected.
418
+ - Input errors identify `OLD` or `NEW` and include the physical line when
419
+ available.
420
+
421
+ No normal summary or details output begins until both inputs pass complete
422
+ input and duplicate validation.
423
+
424
+ ### Identity and duplicates
425
+
426
+ Identity fields are top-level object-member names. Each component must exist
427
+ and be a non-null string, number, or boolean; objects and arrays are invalid.
428
+ Nested identity paths and automatic key detection are not supported.
429
+
430
+ Composite identity field names are sorted lexically before their values are
431
+ extracted. Consequently, `key=("b", "a")` and `key=("a", "b")` both produce
432
+ keys in `(a, b)` order and match identically. Repeated or empty identity-field
433
+ names are configuration errors.
434
+
435
+ JSON types remain significant: `"1"` is different from `1`, and `true` is
436
+ different from `1`.
437
+
438
+ Every normalized identity must be unique within OLD and within NEW by default.
439
+ The first duplicate aborts the comparison; `DuplicateKeyError` reports the
440
+ first occurrence and repeated physical line. Set `duplicates="first"` to keep
441
+ the first occurrence or `duplicates="last"` to keep the last occurrence and
442
+ its physical line. These tolerant policies depend on physical input order.
443
+
444
+ ### Ignored object members
445
+
446
+ Ignore expressions are exact
447
+ [RFC 6901 JSON Pointers](https://www.rfc-editor.org/rfc/rfc6901). Escapes such
448
+ as `~1` for `/` and `~0` for `~` are supported. An absent path is harmless,
449
+ and `/updated_at` does not match `/metadata/updated_at`.
450
+
451
+ Ignores are applied symmetrically before content canonicalization. A pointer
452
+ may remove an entire array-valued object member, but it may not traverse an
453
+ array or address an array element. Ignoring a top-level identity field is also
454
+ invalid. Wildcards, JSONPath, and recursive name matching are not supported.
455
+
456
+ ### Canonical content and numbers
457
+
458
+ Records with the same identity are compared after ignored members are removed:
459
+
460
+ - JSON object property order is insignificant at every depth.
461
+ - Array order is significant.
462
+ - Unicode strings are compared by code-point sequence; no Unicode
463
+ normalization is applied.
464
+ - Numbers are parsed as `Decimal` and compared by mathematical value.
465
+ `1`, `1.0`, and `1e0` are equal, and negative zero equals zero.
466
+ - Arbitrary-precision integers and decimal values are not rounded through
467
+ binary floating point.
468
+
469
+ Canonical content is stored with its length and SHA-256 digest. Matching
470
+ canonical bytes are still compared, so a digest match alone is not treated as
471
+ proof of equality. Numbers are written using compact scientific notation; a
472
+ large exponent does not expand into a large string of zeroes.
473
+
474
+ ### Determinism
475
+
476
+ Summary counts do not depend on either source's physical record order.
477
+ Changes, including details records, use the stable lexical byte order of each
478
+ canonical typed identity. This is a reproducibility guarantee, not numeric,
479
+ locale-aware, or human-oriented sorting.
480
+
481
+ ## Disk-backed architecture and temporary files
482
+
483
+ Each record is parsed and validated incrementally, normalized, and inserted
484
+ into a private SQLite database under an operating-system temporary directory.
485
+ The database stores the typed canonical identity, original line, canonical
486
+ content, content length, and digest. A uniqueness constraint detects duplicate
487
+ identities. SQL joins calculate the summary, and ordered SQLite cursors drive
488
+ lazy change iteration.
489
+
490
+ This architecture bounds memory by the records currently being processed and
491
+ database buffers; it does not keep the complete decoded inputs or all changes
492
+ in RAM. It does require temporary disk space. Canonical records and SQLite
493
+ indexes remain until the `DiffResult` is closed, so temporary usage can exceed
494
+ twice the combined decompressed input size.
495
+
496
+ `max_temp` / `--max-temp` accepts a positive byte count. The implementation
497
+ limits and checks files in the workspace owned by `jsonl-diff`, raising
498
+ `ResourceError` (CLI exit `2`) when the index cannot stay within that budget.
499
+ Choose a limit with room for SQLite pages and index overhead.
500
+
501
+ `py-jsonl` may create its own temporary staging files for remote or compressed
502
+ sources. Those files follow `py-jsonl`'s resource policy and are not counted by
503
+ `jsonl-diff`'s `max_temp` limit. Total system temporary usage can therefore
504
+ exceed the configured value.
505
+
506
+ The private workspace is removed on context-manager exit, explicit `close()`,
507
+ or a handled failure during construction. Cleanup failures are emitted as
508
+ warnings rather than replacing the primary error.
509
+
510
+ ## Limitations and non-goals
511
+
512
+ `jsonl-diff` intentionally does not provide:
513
+
514
+ - line- or physical-order comparison;
515
+ - equal-record events or complete record payloads in details output;
516
+ - field-level diffs, JSON Patch, move detection, or rename detection;
517
+ - nested identities, fuzzy matching, or key autodetection;
518
+ - numeric tolerances, unordered-array comparison, or Unicode normalization;
519
+ - wildcard/JSONPath ignores or ignores that traverse arrays;
520
+ - schema validation or repair of malformed input;
521
+ - ZIP input, database-table input, cloud-provider SDK integrations, GUI, or
522
+ HTML reports.
523
+
524
+ It is a reconciliation tool for strict JSONL datasets, not a general-purpose
525
+ visual JSON diff.
526
+
527
+ ## Development
528
+
529
+ Clone the repository and sync the test and lint dependency groups with `uv`:
530
+
531
+ ```bash
532
+ uv sync --group test --group lint
533
+ ```
534
+
535
+ Run the test suite and Ruff:
536
+
537
+ ```bash
538
+ uv run pytest
539
+ uv run ruff check --quiet --output-format=concise .
540
+ ```
541
+
542
+ The tests cover CLI behavior, identity and canonicalization rules, strict
543
+ validation, details output, source types, compression, temporary limits, and
544
+ resource cleanup.
545
+
546
+ ## License
547
+
548
+ See [LICENSE](LICENSE).